Compare commits
272
Commits
ffbe9959ab
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b6ec59286 | ||
|
|
fdc7f01c26 | ||
|
|
f31b1f62d4 | ||
|
|
7be069d73f | ||
|
|
88373985ec | ||
|
|
7acc49e1fb | ||
|
|
81b3128bb0 | ||
|
|
1d0b3f422d | ||
|
|
0aef0b6c08 | ||
|
|
cd1d6b2d0d | ||
|
|
c888f23901 | ||
|
|
54d02098c8 | ||
|
|
5fc0e8b3cb | ||
|
|
045cdf1f75 | ||
|
|
deed7bdfb0 | ||
|
|
c4d9ade85e | ||
|
|
80daa9f045 | ||
|
|
ef7708bf7d | ||
|
|
750f3aa598 | ||
|
|
c57ee12da1 | ||
|
|
494e16b3b3 | ||
|
|
6bf3b40cc7 | ||
|
|
8743fcc0b5 | ||
|
|
e34dcd6bc6 | ||
|
|
f9fecfc144 | ||
|
|
a59f3132ee | ||
|
|
c7f53e4f7e | ||
|
|
8583bcdf17 | ||
|
|
b12eb0a038 | ||
|
|
b72423c64d | ||
|
|
6b34fc97ec | ||
|
|
401155b501 | ||
|
|
771783d991 | ||
|
|
7ad6487050 | ||
|
|
8ce8978755 | ||
|
|
869cad88e4 | ||
|
|
f136cf3f6b | ||
|
|
a13884d80f | ||
|
|
cd3ee5b5d8 | ||
|
|
93288afa0b | ||
|
|
2c69c51fba | ||
|
|
ce784f8305 | ||
|
|
33013697e0 | ||
|
|
b5b370e6eb | ||
|
|
b9bba643bd | ||
|
|
5ba891ecfb | ||
|
|
efca86c50e | ||
|
|
23a7bde4a5 | ||
|
|
c2d6e37ac4 | ||
|
|
e93d826199 | ||
|
|
404ad2231d | ||
|
|
ef476f3ce7 | ||
|
|
6ee1af41c0 | ||
|
|
e6cb514c3a | ||
|
|
08e5a161f2 | ||
|
|
bbf40774ef | ||
|
|
f6d9cd2cc6 | ||
|
|
9104235f28 | ||
|
|
22d896c7eb | ||
|
|
2ccb5c07ce | ||
|
|
8c7751b96b | ||
|
|
c9ac155145 | ||
|
|
2a839e7b09 | ||
|
|
d47221eb2a | ||
|
|
ff7fbf3ad6 | ||
|
|
47d49e3395 | ||
|
|
f0f5100253 | ||
|
|
09e4f9aa84 | ||
|
|
c6d5b93d77 | ||
|
|
d7aa034661 | ||
|
|
ae3b22df93 | ||
|
|
4f7e586ec3 | ||
|
|
fc3c80d73d | ||
|
|
43e35a71dd | ||
|
|
c18b2c42c2 | ||
|
|
bfed04f449 | ||
|
|
9509eaa65c | ||
|
|
ef41898a60 | ||
|
|
8f9f5932b5 | ||
|
|
f6d7b02a8a | ||
|
|
c8382d3930 | ||
|
|
62c531adb5 | ||
|
|
172f8ede43 | ||
|
|
b69da05003 | ||
|
|
1e56c9d716 | ||
|
|
13d140f82a | ||
|
|
687e0566ac | ||
|
|
3472cc072d | ||
|
|
75b9605982 | ||
|
|
ea97dcea79 | ||
|
|
8e96caa075 | ||
|
|
cfd11848a0 | ||
|
|
4c0c76089d | ||
|
|
517441546d | ||
|
|
182b62da4f | ||
|
|
4686b86d1e | ||
|
|
db7814cd0c | ||
|
|
25a9e11945 | ||
|
|
104875e20e | ||
|
|
cc7a914f14 | ||
|
|
44578b0569 | ||
|
|
45f2598937 | ||
|
|
3324553871 | ||
|
|
4369bec527 | ||
|
|
73f13ecac1 | ||
|
|
14c5a50f3d | ||
|
|
341befc43b | ||
|
|
87ccbbce8e | ||
|
|
b76ae57cf6 | ||
|
|
4e442f70e8 | ||
|
|
2ef28cc5c7 | ||
|
|
5abd25c1a8 | ||
|
|
e746c0c2d2 | ||
|
|
1c8f75f034 | ||
|
|
aace81c2d4 | ||
|
|
ff0f888887 | ||
|
|
0c6ff651d1 | ||
|
|
6ecfd559b9 | ||
|
|
3c92cc406a | ||
|
|
dc4347ef9f | ||
|
|
a74a9ceb2e | ||
|
|
5c133f7302 | ||
|
|
9685fa02be | ||
|
|
90d4c2d0b1 | ||
|
|
72fe2a3ad0 | ||
|
|
a30df8b526 | ||
|
|
e0a383c5b3 | ||
|
|
43f2f8449d | ||
|
|
b3418ad799 | ||
|
|
0eb089d9dc | ||
|
|
22be86d1c2 | ||
|
|
922b225b47 | ||
|
|
30977d2e2b | ||
|
|
78d8730785 | ||
|
|
250f043e4a | ||
|
|
2c3d485af2 | ||
|
|
023217b260 | ||
|
|
db4e84f057 | ||
|
|
f85af3952a | ||
|
|
7d4009da9f | ||
|
|
75c419e0ca | ||
|
|
ce6aa72052 | ||
|
|
071aab72ed | ||
|
|
2cff6edf38 | ||
|
|
423568018e | ||
|
|
fbee943eba | ||
|
|
2f9a87ca24 | ||
|
|
b39bfc8e82 | ||
|
|
9f894b1dea | ||
|
|
67c01cdef3 | ||
|
|
96c85397c6 | ||
|
|
68f6fdef57 | ||
|
|
de0d0bb85e | ||
|
|
2d0b653870 | ||
|
|
bf2d6c396f | ||
|
|
276062fdd4 | ||
|
|
9682abef82 | ||
|
|
c5219ee866 | ||
|
|
a5fd58a7c1 | ||
|
|
85204ca6f0 | ||
|
|
68fbaa631a | ||
|
|
d4d36e8819 | ||
|
|
b761de8b98 | ||
|
|
5dccb76d08 | ||
|
|
3bf172b692 | ||
|
|
1aac95b368 | ||
|
|
73727fbdc4 | ||
|
|
be660210e3 | ||
|
|
43e48c1f26 | ||
|
|
53dfd3be41 | ||
|
|
2f02196864 | ||
|
|
1cd17f53d5 | ||
|
|
955da396c7 | ||
|
|
ccfdbc860e | ||
|
|
e6aa9af283 | ||
|
|
4c38d53972 | ||
|
|
e5304fde29 | ||
|
|
43594af3c8 | ||
|
|
fdcd53d149 | ||
|
|
bc66babd45 | ||
|
|
ac32179bb1 | ||
|
|
cee33c0d1f | ||
|
|
dd9f2f6bbc | ||
|
|
82f1698043 | ||
|
|
7e50c000dc | ||
|
|
d0453881b8 | ||
|
|
7122258d7c | ||
|
|
30d640ca5b | ||
|
|
7c21629404 | ||
|
|
5fdd6ed80c | ||
|
|
38b74cc1db | ||
|
|
7bfdf9c85c | ||
|
|
1367a2257f | ||
|
|
c9ad75b512 | ||
|
|
d9a64d693f | ||
|
|
a70ecb7bfd | ||
|
|
ca0a015ebf | ||
|
|
4ec9685194 | ||
|
|
446a4f28ea | ||
|
|
f70148308b | ||
|
|
eb2f8f5b6f | ||
|
|
1f196d2267 | ||
|
|
297320a75d | ||
|
|
49d27a8a1c | ||
|
|
596ff04902 | ||
|
|
fa72fe03cd | ||
|
|
26a690943d | ||
|
|
c704fbf7a5 | ||
|
|
7ef86c81ca | ||
|
|
0e31aa06b8 | ||
|
|
12cc956329 | ||
|
|
3ce594d9b5 | ||
|
|
0de5929711 | ||
|
|
91cf0ad33f | ||
|
|
a0794a67d1 | ||
|
|
81c6a95f4c | ||
|
|
d8a52eeb4b | ||
|
|
400466b5c5 | ||
|
|
60a3b729aa | ||
|
|
e291a0e2e3 | ||
|
|
425cee15b6 | ||
|
|
62fe25e5c1 | ||
|
|
b4fc51993f | ||
|
|
cb36a2fbb0 | ||
|
|
1a494260db | ||
|
|
04ab117044 | ||
|
|
ad21aa07ed | ||
|
|
2ae3c06c4a | ||
|
|
2b6a1eca19 | ||
|
|
9606187861 | ||
|
|
b423d21b23 | ||
|
|
266e149233 | ||
|
|
026c66a03a | ||
|
|
f1a7b0c2a1 | ||
|
|
dea284357c | ||
|
|
87f1f8be8d | ||
|
|
4405647b33 | ||
|
|
4534b7a17d | ||
|
|
b3e350d40f | ||
|
|
75050bc088 | ||
|
|
f1da40691f | ||
|
|
374fd5a9c2 | ||
|
|
77c8454bb2 | ||
|
|
0ea76a8373 | ||
|
|
0995c2db81 | ||
|
|
6aeebe7826 | ||
|
|
6738279bad | ||
|
|
260ecabb3c | ||
|
|
5241f9784a | ||
|
|
feca8a476c | ||
|
|
999c054bb2 | ||
|
|
80e9b913c1 | ||
|
|
419946f38e | ||
|
|
462f643d6f | ||
|
|
441b5282ef | ||
|
|
50967f6481 | ||
|
|
7dfb4035b7 | ||
|
|
7f45cbff2d | ||
|
|
e12b1302ff | ||
|
|
22c09ecd28 | ||
|
|
a3a91aa2ce | ||
|
|
75ff9274b3 | ||
|
|
e3016a858a | ||
|
|
f0874d8634 | ||
|
|
8d6b48fb4c | ||
|
|
9c9cd8917e | ||
|
|
4b58a159d0 | ||
|
|
f53a2d4f4a | ||
|
|
76daa7026c | ||
|
|
430e3d137c | ||
|
|
de8b521bb4 | ||
|
|
8bd7f5a98f |
No files matched your search
+4
-31
@@ -18,31 +18,12 @@ MONITOR_GUILD_ID=your_guild_id_here # Target guild for text monitoring
|
||||
TEXT_GUILD_ID=optional_text_guild_id # Override text capture guild (falls back to MONITOR_GUILD_ID)
|
||||
TEXT_CHANNEL_ID=optional_text_channel_id # Restrict text capture to a single channel
|
||||
|
||||
# === Voice Channels ===
|
||||
# VOICE_GUILD_ID= # Guild for voice connection (optional)
|
||||
# VOICE_CHANNEL_ID= # Channel for voice connection (optional)
|
||||
|
||||
# === Recording ===
|
||||
RECORDINGS_DIR=./recordings # Audio file output directory (default: ./recordings)
|
||||
RECORDING_SEGMENT_MS=5000 # OGG segment duration in ms (default: 5000)
|
||||
|
||||
# === Decoder ===
|
||||
DECODER_ROTATE_MS=5000 # Opus decoder rotation interval in ms (default: 5000)
|
||||
DECODER_COOLDOWN_MS=30000 # Decoder error cooldown in ms (default: 30000)
|
||||
|
||||
# === Audio ===
|
||||
AUDIO_STREAM_SILENCE_DURATION_MS=3000 # Silence threshold in ms before stopping stream (default: 3000)
|
||||
PACKET_FILTER_MIN_SIZE=8 # Minimum Opus packet size in bytes (default: 8)
|
||||
OPUS_FRAME_SIZE=960 # Opus frame size in samples (default: 960)
|
||||
AUDIO_SAMPLE_RATE=48000 # Audio sample rate in Hz (default: 48000)
|
||||
AUDIO_CHANNELS=2 # Number of audio channels (default: 2)
|
||||
AVATAR_SIZE=64 # User avatar size in pixels (default: 64)
|
||||
|
||||
# === Webserver ===
|
||||
WEBSERVER_PORT=4001 # Backend HTTP/WS server port (default: 4001)
|
||||
|
||||
# === Connection ===
|
||||
VOICE_CONNECTION_TIMEOUT_MS=15000 # Voice connection timeout in ms (default: 15000)
|
||||
RECONNECT_TIMEOUT_MS=5000 # Reconnect timeout in ms (default: 5000)
|
||||
|
||||
# === Logging ===
|
||||
@@ -69,11 +50,6 @@ POSTGRES_POOL_MAX=10 # Maximum pool connections (default: 10)
|
||||
# === Redis ===
|
||||
REDIS_URL=redis://100.121.180.82:6379 # Redis connection string (default: redis://localhost:6379)
|
||||
|
||||
# === Voice PCM WebSocket (direct gateway→backend, bypasses Redis) ===
|
||||
VOICE_PCM_WS_ENABLED=true # Use direct WS for PCM audio (default: true)
|
||||
BACKEND_WS_URL=ws://backend:4001/ws # Backend WebSocket URL for gateway PCM streaming
|
||||
BACKEND_WS_TOKEN= # REQUIRED if VOICE_PCM_WS_ENABLED=true. Internal shared secret
|
||||
|
||||
# === Attachments ===
|
||||
TELE_UPLOAD_URL=https://upload.asepharyana.my.id/api/upload # Attachment upload endpoint (default)
|
||||
ATTACHMENT_UPLOAD_TIMEOUT_MS=30000 # Upload timeout in ms (default: 30000)
|
||||
@@ -88,11 +64,6 @@ AI_LLM_API_KEY= # REQUIRED if AI_ANALYSIS_ENABLED=true. L
|
||||
AI_LLM_BASE_URL=http://100.121.180.82:20128/api/v1 # LLM API base URL (omniroute — OpenAI-compatible router on imrnes, Tailscale 100.121.180.82)
|
||||
AI_LLM_MODEL=text # LLM text model name (default: text)
|
||||
# AI_LLM_VISION_MODEL= # Vision model for image analysis (falls back to AI_LLM_MODEL)
|
||||
# AI_LLM_EMBEDDING_MODEL= # Embedding model for semantic moderation cache (optional; enables near-duplicate text reuse to save LLM calls)
|
||||
# AI_LLM_EMBEDDING_MIN_SIMILARITY=0.97 # Min cosine similarity to reuse a cached verdict (default: 0.97)
|
||||
QDRANT_URL=http://100.121.180.82:6333 # Qdrant vector store for embeddings (semantic cache); when set, vectors are stored/searched in Qdrant instead of Postgres
|
||||
# QDRANT_COLLECTION=gmw_text_moderation # Qdrant collection name (default: gmw_text_moderation)
|
||||
# QDRANT_API_KEY= # Qdrant API key (optional)
|
||||
AI_LLM_MAX_CONCURRENT=5 # Max concurrent LLM API calls (default: 5)
|
||||
AI_LLM_IMAGE_MAX_DIMENSION=1024 # Max image dimension in pixels before resize (default: 1024)
|
||||
AI_LLM_TEXT_BATCH_SIZE=20 # Max messages per text-only moderation batch (default: 20)
|
||||
@@ -126,7 +97,6 @@ AUTO_DELETE_LOG_CHANNEL_ID= # Channel ID to log auto-delete actions
|
||||
# === Retention (0 = disabled) ===
|
||||
RETENTION_MESSAGES_DAYS=0 # Message retention in days (default: 0 = off)
|
||||
RETENTION_ATTACHMENTS_DAYS=0 # Attachment retention in days (default: 0 = off)
|
||||
RETENTION_VOICE_DAYS=0 # Voice recording retention in days (default: 0 = off)
|
||||
RETENTION_CLEANUP_INTERVAL_MS=86400000 # Cleanup interval in ms (default: 24h)
|
||||
RETENTION_DRY_RUN=true # Dry-run: log but do not delete (default: true)
|
||||
|
||||
@@ -134,4 +104,7 @@ RETENTION_DRY_RUN=true # Dry-run: log but do not delete (defaul
|
||||
AUTO_MIGRATE_ON_STARTUP=true # Run database migrations on startup (default: true)
|
||||
|
||||
# === Worker Pool ===
|
||||
# PISCINA_MAX_THREADS=4 # Worker thread pool size (optional, defaults to CPU cores)
|
||||
# Text and media AI-analysis batches run on separate Piscina pools so a slow
|
||||
# image/vision batch can never queue-block fast text-only batches.
|
||||
# PISCINA_MAX_THREADS=4 # Text-analysis worker pool size (optional, defaults to CPU cores)
|
||||
# PISCINA_MEDIA_MAX_THREADS=2 # Media-analysis worker pool size (optional, default 2)
|
||||
@@ -32,29 +32,26 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
run: corepack enable && corepack prepare pnpm@11 --activate
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.14
|
||||
|
||||
- name: Install deps (backend)
|
||||
working-directory: services/backend
|
||||
run: pnpm install --ignore-scripts --no-frozen-lockfile
|
||||
|
||||
- name: Typecheck + test (backend)
|
||||
- name: Install deps + test (backend)
|
||||
working-directory: services/backend
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
./node_modules/.bin/tsc --noEmit
|
||||
# e2e.test.ts requires a live backend (API_BASE) — run unit tests only
|
||||
./node_modules/.bin/vitest run --exclude "src/e2e.test.ts"
|
||||
# src/e2e.test.ts requires a live backend (API_BASE) — unit tests
|
||||
# live in tests/ and are excluded by the bun test dir.
|
||||
bun test tests/
|
||||
|
||||
- name: Install deps (discord-gateway)
|
||||
working-directory: services/discord-gateway
|
||||
run: pnpm install --ignore-scripts --no-frozen-lockfile
|
||||
|
||||
- name: Typecheck + test (discord-gateway)
|
||||
- name: Install deps + test (discord-gateway)
|
||||
working-directory: services/discord-gateway
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
./node_modules/.bin/tsc --noEmit
|
||||
./node_modules/.bin/vitest run
|
||||
bun test tests/
|
||||
|
||||
- name: Biome check (all services)
|
||||
run: |
|
||||
@@ -151,9 +148,17 @@ jobs:
|
||||
|
||||
attic_push_vps_hop() {
|
||||
echo "Fallback: VPS-hop attic push"
|
||||
# Recover the client binary BEFORE the fallback can use it: the
|
||||
# bootstrap cascade below resets ATTIC_BIN="" and never restores it
|
||||
# in the fallback branch, so `sudo $ATTIC_BIN push` used to run as
|
||||
# `sudo push` -> "sudo: 'push': command not found". On the VPS the
|
||||
# closure lives at the canonical ATTIC_DIR path.
|
||||
VPS_ATTIC="/nix/store/fygyy3yk4rqdknxkiwkqambpnhyax0k4-attic-0.1.0/bin/attic"
|
||||
ssh "$VPS_USER@$VPS_HOST" "test -x '$VPS_ATTIC'" \
|
||||
|| ssh "$VPS_USER@$VPS_HOST" "sudo /nix/var/nix/profiles/default/bin/nix-store --realise '$ATTIC_DIR'"
|
||||
# Copy closure to VPS (fast if attic already has it via substitute)
|
||||
ssh "$VPS_USER@$VPS_HOST" "sudo /nix/var/nix/profiles/default/bin/nix-store --realise '$STORE_PATH'" 2>/dev/null \
|
||||
|| nix copy --to "ssh://$VPS_USER@$VPS_HOST" "$STORE_PATH"
|
||||
|| nix copy --to "ssh://***@$VPS_HOST" "$STORE_PATH"
|
||||
# Push from VPS → Attic over Tailscale.
|
||||
# --ignore-upstream-cache-filter is REQUIRED: without it, attic skips
|
||||
# writing the narinfo to gmw when chunks exist in the upstream
|
||||
@@ -162,7 +167,7 @@ jobs:
|
||||
# sudo: attic must read root's config (~/.config/attic), which has
|
||||
# the imrnes-ts server → Tailscale. Non-root users' configs only
|
||||
# have the public `pub` server → "Server imrnes-ts does not exist".
|
||||
ssh "$VPS_USER@$VPS_HOST" "sudo $ATTIC_BIN push imrnes-ts:gmw '$STORE_PATH' --jobs 4 --ignore-upstream-cache-filter" \
|
||||
ssh "$VPS_USER@$VPS_HOST" "sudo $VPS_ATTIC push imrnes-ts:gmw '$STORE_PATH' --jobs 4 --ignore-upstream-cache-filter" \
|
||||
|| echo "attic push failed (non-fatal; ssh copy fallback below)"
|
||||
}
|
||||
|
||||
|
||||
+12
-3
@@ -1,20 +1,29 @@
|
||||
node_modules
|
||||
/recordings
|
||||
.env
|
||||
dist/
|
||||
services/discord-gateway/dist/
|
||||
services/backend/dist/
|
||||
services/frontend/frontend/dist/
|
||||
public/app/
|
||||
.muxer-queue.**
|
||||
.claude/
|
||||
.hermes/
|
||||
.env.test
|
||||
logs/
|
||||
.codegraph/
|
||||
worktrees/
|
||||
.worktrees/
|
||||
services/frontend/frontend/dist/
|
||||
target/
|
||||
nix/
|
||||
# Gitea CI runner logs
|
||||
.gitea/workflows/*.log
|
||||
|
||||
.planning/
|
||||
# Nix build result symlink
|
||||
result
|
||||
|
||||
# Playwright MCP artifacts
|
||||
.playwright-mcp/
|
||||
findings.md
|
||||
findings.md
|
||||
progress.md
|
||||
task_plan.md
|
||||
@@ -1,67 +0,0 @@
|
||||
# Spec: Perbagus fitur Voice + Audio Playback (GMW frontend)
|
||||
|
||||
Tanggal: 2026-08-22 · Scope: **frontend only** (backend/gateway API sudah cukup)
|
||||
|
||||
## Masalah (audit)
|
||||
1. Recordings: semua kartu pakai `<audio controls>` native — tampilan identik,
|
||||
tidak ada indikasi which-clip-playing / loading / paused, dan N audio bisa
|
||||
play bareng (overlap).
|
||||
2. Media view: `thumbnailUrl` dari gateway tidak dipakai; tidak ada visual
|
||||
"sedang playing" selain disc spin; queue item semua sama tanpa badge up-next.
|
||||
3. Mini-player (`lib/hooks/use-media-player.tsx`) ada tapi TIDAK PERNAH
|
||||
dimount → dead code, user tidak lihat status musik di halaman lain.
|
||||
4. Voice page: `useMicTransmit.setVolume` + `useVoiceListen.setVolume`
|
||||
tersedia tapi tak ada UI-nya; mic live tidak punya level feedback.
|
||||
|
||||
## Desain
|
||||
|
||||
### A. RecordingAudioPlayer (baru, `components/voice/recording-audio-player.tsx`)
|
||||
Custom player menggantikan `<audio controls>`:
|
||||
- Play/pause button (ikon berubah), spinner saat buffering (`waiting` event).
|
||||
- Progress bar seekable (click-to-seek) + time label `m:ss / m:ss`.
|
||||
- Waveform-ish equalizer bars saat playing (CSS animation, reduced-motion safe).
|
||||
- **Single-playback**: module-level registry `activePlayers` — memainkan satu
|
||||
clip otomatis pause yang lain.
|
||||
- Kartu pemilik player aktif dapat highlight border signal + "Now playing" chip.
|
||||
|
||||
### B. Recordings view — pasang player baru
|
||||
- Ganti `<audio>` → `<RecordingAudioPlayer src download_url>`.
|
||||
- Highlight kartu via state lifted: `playingId` di view, callback `onPlay`.
|
||||
|
||||
### C. Media view polish
|
||||
- Hero: thumbnail (jika `current.thumbnailUrl`) sebagai disc center image;
|
||||
fallback ListMusic icon. Equalizer bars animasi CSS saat `playing`.
|
||||
- Queue row pertama: badge "up next"; baris current track diberi ring signal.
|
||||
- Volume read-only tetap.
|
||||
|
||||
### D. MiniPlayer global
|
||||
- Hapus `lib/hooks/use-media-player.tsx` (dead) — ganti dengan komponen
|
||||
`components/media/mini-player.tsx` yang subscribe `useMediaState` +
|
||||
`useMediaWsSync` langsung (SWR cache shared antar route), mounted di
|
||||
`AppFrame` bawah layar (fixed bottom, hidden di route `/media`).
|
||||
- Menampilkan: thumbnail kecil/judul, tombol skip/stop, link ke /media.
|
||||
|
||||
### E. Voice UI
|
||||
- Mic live: level meter (Equalizer bars) — mic-transmitter sudah punya worklet;
|
||||
tambah `getLevel()` via AnalyserNode pada stream (simple RMS) di hook.
|
||||
- Listen: volume slider (input range) wired ke `listen.setVolume`.
|
||||
- Mic volume slider wired ke `mic.setVolume`.
|
||||
|
||||
## File touched
|
||||
| File | Aksi |
|
||||
|---|---|
|
||||
| services/frontend/src/components/voice/recording-audio-player.tsx | new |
|
||||
| services/frontend/src/app/(dashboard)/recordings/view.tsx | edit |
|
||||
| services/frontend/src/app/(dashboard)/media/view.tsx | edit |
|
||||
| services/frontend/src/components/media/mini-player.tsx | new |
|
||||
| services/frontend/src/components/shell/ambient-app.tsx | mount MiniPlayer |
|
||||
| services/frontend/src/lib/hooks/use-media-player.tsx | delete |
|
||||
| services/frontend/src/hooks/use-voice.ts | tambah micLevel |
|
||||
| services/frontend/src/lib/audio/mic-transmit.ts | expose analyser level |
|
||||
| services/frontend/src/app/(dashboard)/voice/view.tsx | sliders + meter |
|
||||
|
||||
## Verifikasi
|
||||
1. `pnpm lint` (biome) + `pnpm build` clean.
|
||||
2. Smoke di port **4024** (BUKAN 4017) → curl 200 semua route.
|
||||
3. Commit (tanpa trailer) → push → `gh run watch` → live check
|
||||
https://imphnen.asepharyana.my.id/{media,recordings,voice}/ = 200.
|
||||
@@ -0,0 +1,101 @@
|
||||
# GMW — Agent Guide
|
||||
|
||||
GMW (Guild Moderation Watcher) is a Discord bot + web dashboard for AI-powered moderation. A monorepo with three services: a selfbot gateway that captures Discord events and runs LLM moderation, an Express/oRPC backend that serves the dashboard API, and a Next.js 16 SSR frontend. They communicate via Redis pub/sub (gateway→backend) and WebSocket (backend→browser).
|
||||
|
||||
## Quick reference
|
||||
|
||||
```bash
|
||||
# Per-service — cd into the service first
|
||||
bun install # install deps (bun 1.3.14, not pnpm/npm)
|
||||
bun typecheck # tsc --noEmit
|
||||
bun lint # biome check
|
||||
bun format # biome format --write
|
||||
bun build # gateway/backend: tsc + fix-imports.mjs; frontend: next build
|
||||
bun test # bun test tests/ (gateway & backend only — frontend has no tests)
|
||||
```
|
||||
|
||||
No monorepo-level scripts exist. Run each command from inside the service directory.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
services/
|
||||
├── discord-gateway/ Event-driven selfbot. No HTTP (except :4016 metrics).
|
||||
│ ├── src/
|
||||
│ │ ├── app/ Bootstrap, shutdown, retention
|
||||
│ │ ├── modules/ Feature modules — each self-contained
|
||||
│ │ └── shared/ Config, DB (Drizzle), logger, errors, utils
|
||||
│ ├── tests/ Vitest tests (colocated, not inside src/)
|
||||
│ ├── drizzle/migrations/ DB migrations
|
||||
│ └── scripts/fix-imports.mjs Post-build: rewrites @/ aliases → relative .js
|
||||
│
|
||||
├── backend/ Express HTTP + WebSocket + oRPC server (:4001).
|
||||
│ ├── src/
|
||||
│ │ ├── modules/ Feature modules (schema→repo→service→controller→routes)
|
||||
│ │ ├── http/ Express app setup
|
||||
│ │ ├── ws/ WebSocket server + Redis bridge
|
||||
│ │ ├── orpc/ oRPC router definition
|
||||
│ │ └── shared/ Config, DB, errors, Redis, logger
|
||||
│ └── tests/ Vitest tests
|
||||
│
|
||||
└── frontend/ Next.js 16 App Router, React 19, Tailwind v4 (:4017).
|
||||
├── src/
|
||||
│ ├── src/ Pages — route groups under (dashboard)/
|
||||
│ ├── components/ UI components (primitives, shell, charts, etc.)
|
||||
│ ├── hooks/ React hooks
|
||||
│ └── lib/ API clients, types, utils, WebSocket, audio
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
### Package manager & runtime
|
||||
|
||||
- **bun** (v1.3.14), package manager + test runner. Lockfiles (`bun.lock`) are committed. Node 22 is still used at runtime for the tsc-built `dist/` (via `fix-imports.mjs`), plus bun for dev/install/test. Bun is NOT the production runtime for gateway/backend — the Nix wrapper execs `node dist/index.js` — so native modules resolve against the Node ABI (e.g. `@discordjs/opus` prebuilds for node-v127).
|
||||
- ESM throughout (`"type": "module"` in all package.json files).
|
||||
- Test runner: **bun test** (`bun test tests/`). The old vitest configs are replaced by `bunfig.toml` `[test] preload = ["./tests/setup-env.ts"]` — note bun 1.3.14 **ignores `[test] env`**, so env vars for tests go in the preload file. Bun's jest-compat layer aliases most vitest APIs (`vi.fn`→`jest.fn`, `vi.useFakeTimers`→`jest.useFakeTimers`, `vi.spyOn`→`spyOn`, `vi.mock`→`mock.module`), but there is no `vi.waitFor` or `jest` global — use the small `waitForCompat` helper in `teLine truncated
|
||||
- ESM throughout (`"type": "module"` in all package.json files).
|
||||
|
||||
### Import style
|
||||
|
||||
Source files use `@/*` path aliases (mapped in tsconfig to `./src/*`). Relative imports **must include the `.js` extension** (e.g., `from "./embed.js"`). The `moduleResolution: "bundler"` tsconfig setting allows bare specifiers during dev, but `tsc` emits them as-is. A post-build script (`scripts/fix-imports.mjs`) rewrites both `@/` aliases and extensionless imports in `dist/` so Node ESM can resolve them at runtime.
|
||||
|
||||
### Error handling
|
||||
|
||||
Both gateway and backend define an `AppError` base class in `@/shared/errors/index` with subclasses: `ValidationError` (400), `NotFoundError` (404), `UnauthorizedError` (401), `DatabaseError` (500), `ConfigError` (500). Services throw these; callers or middleware map them to HTTP status codes.
|
||||
|
||||
### Logging
|
||||
|
||||
Use `createChildLogger('module-name')` from `@/shared/logger/index`. Never use raw `console`. It wraps pino; in development it pretty-prints via `pino-pretty`.
|
||||
|
||||
### Config
|
||||
|
||||
Environment variables are validated with Zod at startup in `shared/config/index.ts` of both gateway and backend. Do not read `process.env` directly outside config modules.
|
||||
|
||||
### Module boundaries
|
||||
|
||||
- Gateway: each feature lives in `src/modules/<name>/` with its own `index.ts` barrel. Modules register event listeners and are composed in `src/app/bootstrap.ts`.
|
||||
- Backend: `modules/<name>/` follows schema → repository → service → controller → routes. Data flows up only. No cross-module repository imports.
|
||||
- Frontend: `page.tsx` is a server component that fetches data via `src/lib/api/server.ts` (oRPC over HTTP, server-side only) and passes it to a `view.tsx` client component. Browser code uses `src/lib/orpc/client.ts` (oRPC over WebSocket via partysocket). Never import the server API client from a client component.
|
||||
|
||||
### API layer
|
||||
|
||||
The backend exposes an oRPC router mounted at `/trpc` (both HTTP and WebSocket). The frontend does **not** use a REST `/api/*` layer — all data goes through oRPC procedures. The server-side client (`src/lib/api/server.ts`) uses a fetch-based RPCLink; the browser client uses a WebSocket-based RPCLink backed by partysocket for auto-reconnection. Results are asserted to the frontend's local types at each call site (the backend's router type is not imported into the frontend).
|
||||
|
||||
### Testing
|
||||
|
||||
- **Gateway**: Vitest, tests in `tests/` at the service root. Config sets env vars (`DISCORD_TOKEN`, `DATABASE_URL`, etc.) so tests run without real services.
|
||||
- **Backend**: Vitest, tests in `tests/` and `src/`. Includes an `e2e.test.ts` excluded from CI (needs a live backend).
|
||||
- **Frontend**: No test runner configured.
|
||||
- Tests are pure-function / unit-level. Mock external dependencies; do not start real DB/Redis in tests.
|
||||
|
||||
### Formatting
|
||||
|
||||
Biome, 2-space indent, spaces. Config at repo root `biome.json`. `lint` uses `--diagnostic-level=error`; `format` auto-writes.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
1. **Never commit without running `fix-imports.mjs` after `tsc`** — gateway and backend builds will produce ESM that crashes at startup (`ERR_MODULE_NOT_FOUND`).
|
||||
2. **Don't add `@discordjs/opus` build-from-source flags** — it ships prebuilt binaries for Node 22. Forcing source builds in CI/Nix will fail or add minutes of compile time.
|
||||
3. **Frontend SSR fetches are always `cache: "no-store"`** — the server API client bypasses Next.js fetch cache. Do not add caching without understanding the live dashboard requirement.
|
||||
4. **oRPC types are loosely coupled** — the frontend casts oRPC results to its own types with `as unknown`. Adding a field to the backend schema does not automatically update the frontend type. Update both sides.
|
||||
5. **Gateway is a selfbot** (`discord.js-selfbot-v13`) — it uses a user token, not a bot token. It must not be deployed as a standard Discord bot.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Roadmap
|
||||
|
||||
What is built, what is next, and what has been deliberately declined.
|
||||
|
||||
---
|
||||
|
||||
## Next
|
||||
|
||||
_Empty._
|
||||
@@ -0,0 +1,23 @@
|
||||
# TODO
|
||||
|
||||
Next up. One item, one outcome, verifiable when done.
|
||||
|
||||
Longer-term direction lives in [ROADMAP.md](ROADMAP.md).
|
||||
|
||||
---
|
||||
|
||||
## Now
|
||||
|
||||
_Empty._
|
||||
|
||||
---
|
||||
|
||||
## Next
|
||||
|
||||
_Empty._
|
||||
|
||||
---
|
||||
|
||||
## Maintenance
|
||||
|
||||
_Empty._
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
version: "neuform-top-creators-featured"
|
||||
name: "AeroNet Visualization"
|
||||
description: "Aeronet Visualization Dashboard Section is designed for demonstrating application workflows and interface hierarchy. Key features include clear information density, modular panels, and interface rhythm. It is suitable for product showcases, admin panels, and analytics experiences."
|
||||
colors:
|
||||
primary: "#FFFFFF"
|
||||
secondary: "#000000"
|
||||
accent: "#FFFFFF"
|
||||
background: "#FFFFFF"
|
||||
surface: "#18181B"
|
||||
text-primary: "#111827"
|
||||
text-secondary: "#4B5563"
|
||||
border: "#E5E7EB"
|
||||
typography:
|
||||
display-lg:
|
||||
fontFamily: "Inter"
|
||||
fontSize: "64px"
|
||||
fontWeight: 500
|
||||
lineHeight: "1.04"
|
||||
letterSpacing: "0"
|
||||
body-md:
|
||||
fontFamily: "Inter"
|
||||
fontSize: "16px"
|
||||
fontWeight: 400
|
||||
lineHeight: "1.6"
|
||||
label-md:
|
||||
fontFamily: "JetBrains Mono"
|
||||
fontSize: "12px"
|
||||
fontWeight: 600
|
||||
lineHeight: "1.2"
|
||||
spacing:
|
||||
base: "8px"
|
||||
gap: "16px"
|
||||
card-padding: "24px"
|
||||
section-padding: "80px"
|
||||
rounded:
|
||||
card: "16px"
|
||||
control: "8px"
|
||||
pill: "9999px"
|
||||
components:
|
||||
card:
|
||||
background: "Use the surface token with subtle borders and HTML-matched shadow depth"
|
||||
radius: "Match the declared card radius token"
|
||||
button:
|
||||
background: "Use primary or accent colors for the main action"
|
||||
radius: "Use the control or pill radius based on the source HTML"
|
||||
---
|
||||
# AeroNet Visualization
|
||||
Source: Neuform Featured templates from top creators. Author: Sourasith Phomhome (@madebysourasith). Views: 419; favorites: 14; remixes: 4.
|
||||
Tags: dashboard, animated, threejs, charts, navigation, links, support, effect.
|
||||
## Overview
|
||||
Aeronet Visualization Dashboard Section is designed for demonstrating application workflows and interface hierarchy. Key features include clear information density, modular panels, and interface rhythm. It is suitable for product showcases, admin panels, and analytics experiences.
|
||||
|
||||
AeroNet Mapping Framework Metrics Docs Log In Deploy Node Planet Core Uninterrupted worldwide relay networks driving next-generation infrastructure. 19.5 TB/s +924 820 GB/s +418 Relay established successfully. 2.41 PB/s…
|
||||
## Composition
|
||||
Use the attached HTML reference as the source of truth. Preserve the visible hierarchy, first-screen composition, section rhythm, density, and interaction tone before adapting copy or content.
|
||||
Key visible headings include: Planet Core.
|
||||
## Colors
|
||||
Anchor the palette in primary #FFFFFF, secondary #000000, accent #FFFFFF, background #FFFFFF, surface #18181B, text-primary #111827. Keep background, surface, text, and border roles distinct so generated layouts retain the same contrast pattern as the source.
|
||||
## Typography
|
||||
Use Inter for display moments and Inter for body copy unless the HTML clearly demands a compatible fallback. Labels and technical metadata should use JetBrains Mono or an equivalent mono face.
|
||||
## Layout
|
||||
Keep spacing deliberate and stable. Favor the same grid direction, max-width behavior, card density, and responsive stacking seen in the HTML. Do not replace distinctive source structures with generic SaaS sections.
|
||||
## Components
|
||||
Dashboard, chart, and data panels should preserve their compact operational hierarchy, nested surfaces, and metric emphasis.
|
||||
## Motion
|
||||
Preserve existing motion cues such as masked reveals, staggered entrance, hover lift, scroll-triggered transitions, and ambient movement. Keep easing smooth and restrained.
|
||||
## WebGL & Effects
|
||||
|
||||
If the source includes canvas, WebGL, Three.js, gradients, particles, or atmospheric effects, rebuild them as supporting layers behind the content. Keep effects performant, responsive, and secondary to the interface.
|
||||
|
||||
## Guardrails
|
||||
- Do not flatten the source into a generic card grid.
|
||||
- Do not swap the color mode unless the source clearly supports it.
|
||||
- Preserve the first viewport signal, focal object, and visual density.
|
||||
- Keep buttons, cards, and badges aligned to the same radius and border language.
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
version: "neuform-top-creators-featured"
|
||||
name: "Nexus Capital Render"
|
||||
description: "Nexus Capital Feature Section is designed for highlighting product capabilities and value points. Key features include reusable structure, responsive behavior, and production-ready presentation. It is suitable for component libraries and responsive product interfaces."
|
||||
colors:
|
||||
primary: "#F2EAD3"
|
||||
secondary: "#000000"
|
||||
accent: "#F2EAD3"
|
||||
background: "#000000"
|
||||
surface: "#F2EAD3"
|
||||
text-primary: "#FFFFFF"
|
||||
text-secondary: "#A1A1AA"
|
||||
border: "#27272A"
|
||||
typography:
|
||||
display-lg:
|
||||
fontFamily: "Inter"
|
||||
fontSize: "64px"
|
||||
fontWeight: 500
|
||||
lineHeight: "1.04"
|
||||
letterSpacing: "0"
|
||||
body-md:
|
||||
fontFamily: "Playfair Display"
|
||||
fontSize: "16px"
|
||||
fontWeight: 400
|
||||
lineHeight: "1.6"
|
||||
label-md:
|
||||
fontFamily: "JetBrains Mono"
|
||||
fontSize: "12px"
|
||||
fontWeight: 600
|
||||
lineHeight: "1.2"
|
||||
spacing:
|
||||
base: "8px"
|
||||
gap: "16px"
|
||||
card-padding: "24px"
|
||||
section-padding: "80px"
|
||||
rounded:
|
||||
card: "16px"
|
||||
control: "8px"
|
||||
pill: "9999px"
|
||||
components:
|
||||
card:
|
||||
background: "Use the surface token with subtle borders and HTML-matched shadow depth"
|
||||
radius: "Match the declared card radius token"
|
||||
button:
|
||||
background: "Use primary or accent colors for the main action"
|
||||
radius: "Use the control or pill radius based on the source HTML"
|
||||
---
|
||||
# Nexus Capital Render
|
||||
Source: Neuform Featured templates from top creators. Author: Meng To (@mengto). Views: 311; favorites: 22; remixes: 15.
|
||||
Tags: feature, section, animated, bento, charts.
|
||||
## Overview
|
||||
Nexus Capital Feature Section is designed for highlighting product capabilities and value points. Key features include reusable structure, responsive behavior, and production-ready presentation. It is suitable for component libraries and responsive product interfaces.
|
||||
|
||||
NEXUS WEALTH CORP. Beyond traditional equity. The multi-asset strategy. A dynamic portfolio framework where capital transitions seamlessly from baseline reserves to high-yield synthetic instruments. Drag the timeline to…
|
||||
## Composition
|
||||
Use the attached HTML reference as the source of truth. Preserve the visible hierarchy, first-screen composition, section rhythm, density, and interaction tone before adapting copy or content.
|
||||
Key visible headings include: Beyond traditional equity. The multi-asset strategy..
|
||||
## Colors
|
||||
Anchor the palette in primary #F2EAD3, secondary #000000, accent #F2EAD3, background #000000, surface #F2EAD3, text-primary #FFFFFF. Keep background, surface, text, and border roles distinct so generated layouts retain the same contrast pattern as the source.
|
||||
## Typography
|
||||
Use Inter for display moments and Playfair Display for body copy unless the HTML clearly demands a compatible fallback. Labels and technical metadata should use JetBrains Mono or an equivalent mono face.
|
||||
## Layout
|
||||
Keep spacing deliberate and stable. Favor the same grid direction, max-width behavior, card density, and responsive stacking seen in the HTML. Do not replace distinctive source structures with generic SaaS sections.
|
||||
## Components
|
||||
Dashboard, chart, and data panels should preserve their compact operational hierarchy, nested surfaces, and metric emphasis.
|
||||
## Motion
|
||||
Preserve existing motion cues such as masked reveals, staggered entrance, hover lift, scroll-triggered transitions, and ambient movement. Keep easing smooth and restrained.
|
||||
## WebGL & Effects
|
||||
|
||||
If the source includes canvas, WebGL, Three.js, gradients, particles, or atmospheric effects, rebuild them as supporting layers behind the content. Keep effects performant, responsive, and secondary to the interface.
|
||||
|
||||
## Guardrails
|
||||
- Do not flatten the source into a generic card grid.
|
||||
- Do not swap the color mode unless the source clearly supports it.
|
||||
- Preserve the first viewport signal, focal object, and visual density.
|
||||
- Keep buttons, cards, and badges aligned to the same radius and border language.
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
version: "neuform-staff-featured-2026-05-22"
|
||||
name: "Summit — Cloud Migration Platform"
|
||||
description: "Summit Cloud Pricing Section is designed for comparing plans and supporting conversion decisions. Key features include plan comparison blocks and conversion-oriented actions. It is suitable for subscription pricing pages and plan comparison experiences."
|
||||
colors:
|
||||
primary: "#2563EB"
|
||||
secondary: "#050510"
|
||||
accent: "#3B82F6"
|
||||
background: "#050510"
|
||||
surface: "#18181B"
|
||||
text-primary: "#FFFFFF"
|
||||
text-secondary: "#A1A1AA"
|
||||
border: "#27272A"
|
||||
typography:
|
||||
display-lg:
|
||||
fontFamily: "Inter"
|
||||
fontSize: "64px"
|
||||
fontWeight: 500
|
||||
lineHeight: "1.04"
|
||||
letterSpacing: "0"
|
||||
body-md:
|
||||
fontFamily: "Inter"
|
||||
fontSize: "16px"
|
||||
fontWeight: 400
|
||||
lineHeight: "1.6"
|
||||
label-md:
|
||||
fontFamily: "JetBrains Mono"
|
||||
fontSize: "12px"
|
||||
fontWeight: 600
|
||||
lineHeight: "1.2"
|
||||
spacing:
|
||||
base: "8px"
|
||||
gap: "16px"
|
||||
card-padding: "24px"
|
||||
section-padding: "80px"
|
||||
rounded:
|
||||
card: "23px"
|
||||
control: "18px"
|
||||
pill: "9999px"
|
||||
components:
|
||||
card:
|
||||
background: "Use the surface token with subtle borders and HTML-matched shadow depth"
|
||||
radius: "Match the declared card radius token"
|
||||
button:
|
||||
background: "Use primary or accent colors for the main action"
|
||||
radius: "Use the control or pill radius based on the source HTML"
|
||||
---
|
||||
# Summit — Cloud Migration Platform
|
||||
Source: Neuform staff featured templates. Author: Vanh DesignCode (@vanh). Views: 161; favorites: 11; remixes: 3.
|
||||
Tags: pricing, section, animated, cta, charts, billing.
|
||||
## Overview
|
||||
Summit Cloud Pricing Section is designed for comparing plans and supporting conversion decisions. Key features include plan comparison blocks and conversion-oriented actions. It is suitable for subscription pricing pages and plan comparison experiences.
|
||||
|
||||
Summit Platform Solutions Pricing Docs Start Migration Now in General Availability Migrate to the cloud with confidence Automated discovery, dependency mapping, and zero-downtime migration for enterprise workloads. Move…
|
||||
## Composition
|
||||
Use the attached HTML reference as the source of truth. Preserve the visible hierarchy, first-screen composition, section rhythm, density, and interaction tone before adapting copy or content.
|
||||
Key visible headings include: Migrate to the cloud with confidence; Discover untethered scalability. The platform built to dynamically map, migrate, and optimize your entire infrastructure without lifting a finger.; Architectural Clarity; Dynamic Scaling.
|
||||
## Colors
|
||||
Anchor the palette in primary #2563EB, secondary #050510, accent #3B82F6, background #050510, surface #18181B, text-primary #FFFFFF. Keep background, surface, text, and border roles distinct so generated layouts retain the same contrast pattern as the source.
|
||||
## Typography
|
||||
Use Inter for display moments and Inter for body copy unless the HTML clearly demands a compatible fallback. Labels and technical metadata should use JetBrains Mono or an equivalent mono face.
|
||||
## Layout
|
||||
Keep spacing deliberate and stable. Favor the same grid direction, max-width behavior, card density, and responsive stacking seen in the HTML. Do not replace distinctive source structures with generic SaaS sections.
|
||||
## Components
|
||||
Dashboard, chart, and data panels should preserve their compact operational hierarchy, nested surfaces, and metric emphasis.
|
||||
## Motion
|
||||
Preserve existing motion cues such as masked reveals, staggered entrance, hover lift, scroll-triggered transitions, and ambient movement. Keep easing smooth and restrained.
|
||||
## WebGL & Effects
|
||||
|
||||
If the source includes canvas, WebGL, Three.js, gradients, particles, or atmospheric effects, rebuild them as supporting layers behind the content. Keep effects performant, responsive, and secondary to the interface.
|
||||
|
||||
## Guardrails
|
||||
- Do not flatten the source into a generic card grid.
|
||||
- Do not swap the color mode unless the source clearly supports it.
|
||||
- Preserve the first viewport signal, focal object, and visual density.
|
||||
- Keep buttons, cards, and badges aligned to the same radius and border language.
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
# docs/ — Spec-Driven Development Hub
|
||||
|
||||
Direktori ini adalah pusat dari workflow **spec-driven development** di GMW.
|
||||
Semua perubahan signifikan dimulai dari satu spec, **sebelum kode ditulis**,
|
||||
dan setiap tugas yang berjalan dilacak lewat `todo.md`.
|
||||
|
||||
## Struktur
|
||||
|
||||
```
|
||||
docs/
|
||||
├── README.md # Dokumen ini
|
||||
├── spec-template.md # Template standar untuk semua spec
|
||||
├── todo-template.md # Template todo list
|
||||
└── specs/ # Semua spec dan implementation plan
|
||||
├── YYYY-MM-DD_<slug>-spec.md # Spec (apa & mengapa)
|
||||
└── YYYY-MM-DD_<slug>.md # Plan/fix (bisa langsung spec+plan)
|
||||
```
|
||||
|
||||
### Naming convention
|
||||
|
||||
```
|
||||
YYYY-MM-DD_<slug>-spec.md # Spec baru untuk fitur/fix
|
||||
YYYY-MM-DD_<slug>.md # Plan yang sudah include spec di dalamnya
|
||||
```
|
||||
|
||||
Contoh:
|
||||
- `2026-08-30_recordings-v2-features-spec.md` — spec untuk Recordings v2
|
||||
- `2026-08-24-attachment-delay-fix.md` — spec + plan untuk attachment delay fix
|
||||
|
||||
### Service-specific docs
|
||||
|
||||
```
|
||||
services/<service>/docs/specs/ # Spec yang spesifik untuk 1 service
|
||||
```
|
||||
|
||||
## Workflow: Spec-Driven Development
|
||||
|
||||
### Prinsip utama
|
||||
|
||||
> **No code without a spec.** Semua perubahan signifikan harus punya spec
|
||||
> terlebih dahulu. Spec adalah kontrak: apa yang akan dibangun, mengapa,
|
||||
> dan bagaimana memverifikasinya.
|
||||
|
||||
### Todo list (`todo.md`) — WAJIB
|
||||
|
||||
Setiap tugas berjalan (implementasi fitur/fix) harus punya `todo.md`:
|
||||
|
||||
```markdown
|
||||
# Todo — <judul tugas>
|
||||
|
||||
## Task
|
||||
- [ ] Tulis spec
|
||||
- [ ] Verifikasi facts (baca kode: file:line)
|
||||
- [ ] Implementasi tahap 1: ...
|
||||
- [ ] Implementasi tahap 2: ...
|
||||
- [ ] Verifikasi: pnpm typecheck / lint / build / test
|
||||
- [ ] Commit + push
|
||||
```
|
||||
|
||||
Aturan `todo.md`:
|
||||
- Task **konkret & verifiable** — "ubah X di file Y", bukan "fix bug".
|
||||
- Satu task `[in_progress]` pada satu waktu; ceklis `[x]` segera setelah selesai.
|
||||
- Gunakan `docs/todo-template.md` sebagai template.
|
||||
- Simpan `todo.md` di `docs/` (tugas lintas-service) atau di
|
||||
`services/<service>/docs/` (tugas satu service).
|
||||
|
||||
### Kapan perlu spec + todo
|
||||
|
||||
| Perlu spec + todo | Tidak perlu |
|
||||
|---|---|
|
||||
| Fitur baru | Typo fix |
|
||||
| Bug fix non-trivial | Dependency bump (dependabot) |
|
||||
| Refactor yang mengubah behavior | Format/lint auto-fix |
|
||||
| Perubahan DB schema | Test-only change |
|
||||
| Perubahan API contract | README/doc update |
|
||||
| Perubahan arsitektur | Variable rename |
|
||||
|
||||
### Workflow step-by-step
|
||||
|
||||
1. **Tulis spec** — Salin `docs/spec-template.md`, isi semua section yang relevan.
|
||||
- **Verified facts**: baca kode yang terpengaruh, catat temuan dengan file:line.
|
||||
- **Root cause**: analisis sebab (jangan hanya describe symptom).
|
||||
- **Decisions**: pilih pendekatan, jelaskan alternatif yang ditolak.
|
||||
- **Verification**: command executable, bukan "should work".
|
||||
- Simpan sebagai `docs/specs/YYYY-MM-DD_<slug>-spec.md`.
|
||||
|
||||
2. **Tulis `todo.md`** — Pecah spec jadi task konkret yang bisa diceklis
|
||||
(gunakan `docs/todo-template.md`), dengan urutan implementasi yang logis.
|
||||
|
||||
3. **Review spec** — Baca ulang spec sendiri. Cek:
|
||||
- Apakah verified facts benar-benar verified (bukan asumsi)?
|
||||
- Apakah file changes lengkap (tidak ada yang terlewat)?
|
||||
- Apakah verification steps executable?
|
||||
- Jika spec untuk user request: pastikan user setuju dengan approach.
|
||||
|
||||
4. **Implement** — Ikuti spec + todo step-by-step. Ceklis `[x]` setiap task
|
||||
yang selesai. Jika menemukan sesuatu yang berubah dari asumsi spec,
|
||||
**update spec dulu**, baru implement.
|
||||
|
||||
5. **Verify** — Jalankan semua verification steps di spec. Catat hasilnya.
|
||||
|
||||
6. **Commit** — Reference spec di commit message:
|
||||
```
|
||||
feat(module): deskripsi singkat
|
||||
|
||||
Ref: docs/specs/YYYY-MM-DD_<slug>-spec.md
|
||||
```
|
||||
|
||||
### Tips menulis spec yang baik
|
||||
|
||||
- **Evidence-based**: setiap klaim harus ada sumbernya (file:line, log, error).
|
||||
- **Actionable**: orang lain (atau AI agent) harus bisa implementasi dari spec saja.
|
||||
- **Verifiable**: setiap requirement harus bisa di-test secara eksplisit.
|
||||
- **Scoped**: satu spec = satu perubahan terfokus. Jangan campur 3 fitur dalam 1 spec.
|
||||
- **Bahasa campuran**: narrative boleh Indonesia/English, istilah teknis pakai English.
|
||||
|
||||
### Role of AI agents
|
||||
|
||||
AI coding agents (Shiro Neko / Hermes) **harus**:
|
||||
- Membaca spec sebelum menulis kode
|
||||
- Membuat & meng-update `todo.md` untuk setiap tugas berjalan
|
||||
- Verifikasi fakta dari kode aktual (bukan dari asumsi training data)
|
||||
- Update spec jika temuan baru mengubah approach
|
||||
- Jalankan verification steps setelah implementasi
|
||||
- Reference spec di commit message
|
||||
|
||||
AI agents **tidak boleh**:
|
||||
- Langsung implement tanpa membaca/menulis spec
|
||||
- Mengabaikan verified facts yang bertentangan dengan asumsi
|
||||
- Skip verification steps
|
||||
- Mengerjakan tugas tanpa todo list yang jelas
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"diagram_type": "architecture",
|
||||
"meta": {
|
||||
"title": "GMW — Guild Moderation Watcher",
|
||||
"output": "gmw-architecture.html",
|
||||
"quality_profile": "showcase",
|
||||
"viewBox": [1140, 620]
|
||||
},
|
||||
"components": [
|
||||
{ "id": "redis", "type": "messagebus", "label": "Redis", "sublabel": "pub/sub :6379", "pos": [300, 40], "size": [140, 60], "tag": "events + PCM" },
|
||||
{ "id": "pg", "type": "database", "label": "PostgreSQL", "sublabel": "shared state :6432", "pos": [640, 40], "size": [140, 60] },
|
||||
{ "id": "discord", "type": "external", "label": "Discord Guilds", "sublabel": "channels · voice · reactions", "pos": [40, 240], "size": [140, 60] },
|
||||
{ "id": "gateway", "type": "backend", "label": "discord-gateway", "sublabel": "selfbot :4016", "pos": [260, 240], "size": [140, 60], "tag": "discord.js-selfbot-v13" },
|
||||
{ "id": "backend", "type": "backend", "label": "gmw-backend", "sublabel": "Express · oRPC · WS :4001", "pos": [640, 240], "size": [140, 60], "tag": "Drizzle ORM" },
|
||||
{ "id": "proxy", "type": "cloud", "label": "nginx proxy", "sublabel": "reverse proxy :4009", "pos": [960, 240], "size": [140, 60], "tag": "nginx" },
|
||||
{ "id": "ai", "type": "backend", "label": "AI Moderation", "sublabel": "LLM caller · vision", "pos": [260, 400], "size": [140, 60] },
|
||||
{ "id": "frontend", "type": "frontend", "label": "gmw-frontend", "sublabel": "Next.js 16 SSR :4017", "pos": [960, 400], "size": [140, 60], "tag": "React 19 · Tailwind v4" },
|
||||
{ "id": "llm", "type": "cloud", "label": "9router LLM", "sublabel": "text + vision API", "pos": [260, 540], "size": [140, 60], "tag": "AI_LLM_BASE_URL" },
|
||||
{ "id": "browser", "type": "external", "label": "Dashboard Users", "sublabel": "browser · partysocket", "pos": [960, 540], "size": [140, 60] }
|
||||
],
|
||||
"boundaries": [
|
||||
{ "kind": "region", "label": "imrnes — Tailscale shared state", "wraps": ["redis", "pg"] },
|
||||
{ "kind": "region", "label": "orangevps — Nix/systemd", "wraps": ["gateway", "ai", "backend", "proxy", "frontend"] }
|
||||
],
|
||||
"connections": [
|
||||
{ "id": "c1", "from": "discord", "to": "gateway", "label": "selfbot events", "variant": "emphasis" },
|
||||
{ "id": "c2", "from": "gateway", "to": "ai", "label": "capture → analyze", "labelDy": 24 },
|
||||
{ "id": "c3", "from": "gateway", "to": "redis", "label": "publish events", "fromSide": "top", "toSide": "bottom" },
|
||||
{ "id": "c4", "from": "redis", "to": "backend", "label": "redis-bridge → WS" },
|
||||
{ "id": "c5", "from": "ai", "to": "llm", "label": "text + vision", "labelDy": 24 },
|
||||
{ "id": "c6", "from": "backend", "to": "llm", "label": "agentic chatbot", "variant": "dashed" },
|
||||
{ "id": "c7", "from": "backend", "to": "pg", "label": "Drizzle SQL" },
|
||||
{ "id": "c8", "from": "proxy", "to": "backend", "label": "/api · /ws · /trpc" },
|
||||
{ "id": "c9", "from": "proxy", "to": "frontend", "label": "SSR + static", "labelDy": 24 },
|
||||
{ "id": "c10", "from": "browser", "to": "frontend", "label": "HTTPS dashboard", "variant": "emphasis" }
|
||||
],
|
||||
"cards": [
|
||||
{ "dot": "cyan", "title": "Capture", "items": ["Selfbot gateway listens to guild events + voice streams", "AI moderation runs LLM on messages, attachments, transcripts"] },
|
||||
{ "dot": "emerald", "title": "Application", "items": ["oRPC over HTTP + WS (partysocket auto-reconnect)", "Next.js 16 SSR dashboard · nginx :4009 reverse proxy"] },
|
||||
{ "dot": "rose", "title": "Shared State", "items": ["Redis pub/sub relays events + live PCM audio", "PostgreSQL via Drizzle persists messages & assessments"] }
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
@@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Archify automated browser evidence · gmw-architecture.html</title>
|
||||
<style>
|
||||
*{box-sizing:border-box}body{margin:0;padding:24px;background:#e9eef5;color:#172033;font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}header{max-width:1500px;margin:0 auto 18px}h1{margin:0 0 6px;font-size:20px}p{margin:0;color:#526176}.grid{max-width:1500px;margin:auto;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}figure{margin:0;padding:10px;background:white;border:1px solid #c9d4e3;border-radius:12px;box-shadow:0 10px 30px rgba(15,23,42,.08)}img{display:Line truncated
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><h1>Automated browser evidence</h1><p>gmw-architecture.html · visual-check containment pass · perceptual visual review pending</p></header>
|
||||
<main class="grid">
|
||||
<figure>
|
||||
<img src="gmw-architecture.visual-check.1440x900.light.png" alt="light 1440 by 900">
|
||||
<figcaption><strong>LIGHT</strong> · 1440×900 · containment pass</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="gmw-architecture.visual-check.1440x900.dark.png" alt="dark 1440 by 900">
|
||||
<figcaption><strong>DARK</strong> · 1440×900 · containment pass</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="gmw-architecture.visual-check.2048x1320.light.png" alt="light 2048 by 1320">
|
||||
<figcaption><strong>LIGHT</strong> · 2048×1320 · containment pass</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="gmw-architecture.visual-check.2048x1320.dark.png" alt="dark 2048 by 1320">
|
||||
<figcaption><strong>DARK</strong> · 2048×1320 · containment pass</figcaption>
|
||||
</figure>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,548 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"ok": true,
|
||||
"command": "visual-check",
|
||||
"evidenceKind": "automated-browser",
|
||||
"status": "pass",
|
||||
"visualReview": "pending",
|
||||
"artifact": {
|
||||
"path": "/home/code/GMW/docs/diagrams/gmw-architecture.html",
|
||||
"sha256": "8a9646053eb562bd79f674764521f96856d74da603476459c0aeedce53ab7573",
|
||||
"bytes": 811554
|
||||
},
|
||||
"state": {
|
||||
"detail": "read",
|
||||
"motion": "still"
|
||||
},
|
||||
"chrome": {
|
||||
"status": "available",
|
||||
"executable": "/bin/google-chrome"
|
||||
},
|
||||
"diagnostics": [],
|
||||
"containment": {
|
||||
"status": "pass",
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"theme": "light",
|
||||
"innerWidth": 1440,
|
||||
"innerHeight": 900,
|
||||
"scrollWidth": 1440,
|
||||
"scrollHeight": 900,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1163,
|
||||
"diagramWidth": 1133,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.752105263157895,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 51,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "light"
|
||||
},
|
||||
{
|
||||
"width": 1600,
|
||||
"height": 1000,
|
||||
"theme": "light",
|
||||
"innerWidth": 1600,
|
||||
"innerHeight": 1000,
|
||||
"scrollWidth": 1600,
|
||||
"scrollHeight": 1000,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1207,
|
||||
"diagramWidth": 1177,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.8,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 51,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "light"
|
||||
},
|
||||
{
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"theme": "light",
|
||||
"innerWidth": 1920,
|
||||
"innerHeight": 1080,
|
||||
"scrollWidth": 1920,
|
||||
"scrollHeight": 1080,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1383,
|
||||
"diagramWidth": 1353,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.8,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 51,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "light"
|
||||
},
|
||||
{
|
||||
"width": 2048,
|
||||
"height": 1320,
|
||||
"theme": "light",
|
||||
"innerWidth": 2048,
|
||||
"innerHeight": 1320,
|
||||
"scrollWidth": 2048,
|
||||
"scrollHeight": 1320,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1797,
|
||||
"diagramWidth": 1747,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.8,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 41,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "light"
|
||||
}
|
||||
]
|
||||
},
|
||||
"readability": {
|
||||
"status": "pass",
|
||||
"minimumProjectedNodeTextPx": 6,
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"theme": "light",
|
||||
"innerWidth": 1440,
|
||||
"innerHeight": 900,
|
||||
"scrollWidth": 1440,
|
||||
"scrollHeight": 900,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1163,
|
||||
"diagramWidth": 1133,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.752105263157895,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 51,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "light"
|
||||
},
|
||||
{
|
||||
"width": 1600,
|
||||
"height": 1000,
|
||||
"theme": "light",
|
||||
"innerWidth": 1600,
|
||||
"innerHeight": 1000,
|
||||
"scrollWidth": 1600,
|
||||
"scrollHeight": 1000,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1207,
|
||||
"diagramWidth": 1177,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.8,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 51,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "light"
|
||||
},
|
||||
{
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"theme": "light",
|
||||
"innerWidth": 1920,
|
||||
"innerHeight": 1080,
|
||||
"scrollWidth": 1920,
|
||||
"scrollHeight": 1080,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1383,
|
||||
"diagramWidth": 1353,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.8,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 51,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "light"
|
||||
},
|
||||
{
|
||||
"width": 2048,
|
||||
"height": 1320,
|
||||
"theme": "light",
|
||||
"innerWidth": 2048,
|
||||
"innerHeight": 1320,
|
||||
"scrollWidth": 2048,
|
||||
"scrollHeight": 1320,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1797,
|
||||
"diagramWidth": 1747,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.8,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 41,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "light"
|
||||
}
|
||||
]
|
||||
},
|
||||
"viewerChrome": {
|
||||
"status": "pass",
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"theme": "light",
|
||||
"innerWidth": 1440,
|
||||
"innerHeight": 900,
|
||||
"scrollWidth": 1440,
|
||||
"scrollHeight": 900,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1163,
|
||||
"diagramWidth": 1133,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.752105263157895,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 51,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "light"
|
||||
},
|
||||
{
|
||||
"width": 1600,
|
||||
"height": 1000,
|
||||
"theme": "light",
|
||||
"innerWidth": 1600,
|
||||
"innerHeight": 1000,
|
||||
"scrollWidth": 1600,
|
||||
"scrollHeight": 1000,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1207,
|
||||
"diagramWidth": 1177,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.8,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 51,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "light"
|
||||
},
|
||||
{
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"theme": "light",
|
||||
"innerWidth": 1920,
|
||||
"innerHeight": 1080,
|
||||
"scrollWidth": 1920,
|
||||
"scrollHeight": 1080,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1383,
|
||||
"diagramWidth": 1353,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.8,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 51,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "light"
|
||||
},
|
||||
{
|
||||
"width": 2048,
|
||||
"height": 1320,
|
||||
"theme": "light",
|
||||
"innerWidth": 2048,
|
||||
"innerHeight": 1320,
|
||||
"scrollWidth": 2048,
|
||||
"scrollHeight": 1320,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1797,
|
||||
"diagramWidth": 1747,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.8,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 41,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "light"
|
||||
}
|
||||
]
|
||||
},
|
||||
"captures": {
|
||||
"status": "pass",
|
||||
"screenshots": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"theme": "light",
|
||||
"innerWidth": 1440,
|
||||
"innerHeight": 900,
|
||||
"scrollWidth": 1440,
|
||||
"scrollHeight": 900,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1163,
|
||||
"diagramWidth": 1133,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.752105263157895,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 51,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "light",
|
||||
"file": "gmw-architecture.visual-check.1440x900.light.png"
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"theme": "dark",
|
||||
"innerWidth": 1440,
|
||||
"innerHeight": 900,
|
||||
"scrollWidth": 1440,
|
||||
"scrollHeight": 900,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1163,
|
||||
"diagramWidth": 1133,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.752105263157895,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 51,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "dark",
|
||||
"file": "gmw-architecture.visual-check.1440x900.dark.png"
|
||||
},
|
||||
{
|
||||
"width": 2048,
|
||||
"height": 1320,
|
||||
"theme": "light",
|
||||
"innerWidth": 2048,
|
||||
"innerHeight": 1320,
|
||||
"scrollWidth": 2048,
|
||||
"scrollHeight": 1320,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1797,
|
||||
"diagramWidth": 1747,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.8,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 41,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "light",
|
||||
"file": "gmw-architecture.visual-check.2048x1320.light.png"
|
||||
},
|
||||
{
|
||||
"width": 2048,
|
||||
"height": 1320,
|
||||
"theme": "dark",
|
||||
"innerWidth": 2048,
|
||||
"innerHeight": 1320,
|
||||
"scrollWidth": 2048,
|
||||
"scrollHeight": 1320,
|
||||
"overflowX": false,
|
||||
"overflowY": false,
|
||||
"ok": true,
|
||||
"readerWidth": 1797,
|
||||
"diagramWidth": 1747,
|
||||
"viewBoxWidth": 1140,
|
||||
"minimumProjectedNodeTextPx": 7.8,
|
||||
"minimumProjectedNodeText": "channels · voice · reactions",
|
||||
"minimumProjectedNodeTextDetail": "context",
|
||||
"minimumRequiredNodeTextPx": 6,
|
||||
"readabilityOk": true,
|
||||
"hasLegend": false,
|
||||
"hasNavigationDock": true,
|
||||
"legendDockIntersectionArea": 0,
|
||||
"dockStageIntersectionArea": 0,
|
||||
"dockStageGap": 10.21875,
|
||||
"requiredDockStageGap": 10,
|
||||
"viewerChromeStageOk": true,
|
||||
"viewerChromeReserve": 41,
|
||||
"viewerChromeActive": true,
|
||||
"viewerChromeOk": true,
|
||||
"resolvedTheme": "dark",
|
||||
"file": "gmw-architecture.visual-check.2048x1320.dark.png"
|
||||
}
|
||||
],
|
||||
"contactSheet": "gmw-architecture.visual-check.html"
|
||||
},
|
||||
"sidecars": {
|
||||
"receipt": "gmw-architecture.visual-check.json",
|
||||
"contactSheet": "gmw-architecture.visual-check.html"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# Spec: <Judul singkat, aktif, deskriptif>
|
||||
|
||||
Status: **DRAFT** | **APPROVED** | **IN PROGRESS** | **DONE**
|
||||
Date: YYYY-MM-DD
|
||||
Author: <nama/agent>
|
||||
Related: <link ke spec/plan terkait jika ada>
|
||||
Todo: <link ke todo.md untuk tugas ini, jika ada>
|
||||
|
||||
## Problem
|
||||
|
||||
<Deskripsi masalah atau keinginan. Apa yang rusak? Apa yang belum ada?
|
||||
Sebutkan siapa yang melaporkan (user/bot/audit) dan konteksnya.
|
||||
Gunakan **evidence**: log, error, metrik, atau observasi langsung.>
|
||||
|
||||
## Root cause
|
||||
|
||||
<Analisis teknis mengapa masalah ini terjadi. Jika bug: trace dari symptom ke cause.
|
||||
Jika fitur baru: jelaskan gap saat ini. Referensikan file + line number yang relevan.>
|
||||
|
||||
## Behavior target
|
||||
|
||||
<Deskripsi eksplisit perilaku yang diharapkan setelah fix/fitur.
|
||||
Buat list bernomor. Setiap item harus bisa di-verify.>
|
||||
|
||||
## Verified facts
|
||||
|
||||
>Fakta-fakta teknis yang **sudah diverifikasi** dari pembacaan kode, log produksi,
|
||||
>atau eksperimen langsung. Setiap fakta harus citation ke file:line.
|
||||
>Jika belum diverifikasi, tulis `UNVERIFIED` dan rencana verifikasi.
|
||||
|
||||
- `path/to/file.ts:42` — <apa yang terjadi di sini>
|
||||
- `path/to/file.ts:88` — <apa yang terjadi di sini>
|
||||
- Kolom DB `table.column` — <tipe, constraint, index>
|
||||
- Config `ENV_VAR` default <value> — <dibaca di mana>
|
||||
|
||||
## Keputusan desain
|
||||
|
||||
<Pilihan desain yang dibuat, beserta rationale (mengapa bukan alternatif lain).
|
||||
Format: nomor, judul singkat, penjelasan.>
|
||||
|
||||
1. **<Judul keputusan>**: <penjelasan + rationale>
|
||||
- Alternatif yang ditolak: <apa + mengapa ditolak>
|
||||
|
||||
## Perubahan file
|
||||
|
||||
>Daftar **semua file** yang perlu diubah/ditambah/dihapus, dikelompokkan per service.
|
||||
>Untuk setiap file: sebutkan **apa yang berubah** (bukan copy-paste kode).
|
||||
|
||||
### Gateway (`services/discord-gateway/`)
|
||||
- `src/modules/<module>/<file>.ts` — <ringkasan perubahan>
|
||||
|
||||
### Backend (`services/backend/`)
|
||||
- `src/modules/<module>/<file>.ts` — <ringkasan perubahan>
|
||||
|
||||
### Frontend (`services/frontend/`)
|
||||
- `src/<path>/<file>.ts|tsx` — <ringkasan perubahan>
|
||||
|
||||
### Database / Config
|
||||
- <migration file atau config change jika ada>
|
||||
|
||||
## Schema/type changes
|
||||
|
||||
<Perubahan tipe, interface, atau DB schema. Jika tidak ada, tulis "TIDAK ada perubahan.">
|
||||
|
||||
## Verification
|
||||
|
||||
>**Harus spesifik dan executable.** Jangan tulis "works correctly".
|
||||
>Tulis command yang bisa dijalankan dan expected outcome.
|
||||
|
||||
- **Typecheck**: `<service> pnpm typecheck` — clean
|
||||
- **Lint**: `<service> pnpm lint` — no errors
|
||||
- **Build**: `<service> pnpm build` — compiles
|
||||
- **Test**: `<service> pnpm test` — all pass (+ test baru jika ada)
|
||||
- **Smoke test**: <langkah manual untuk verifikasi visual/behavioral>
|
||||
- **DB**: <query untuk cek data jika applicable>
|
||||
- **Deploy**: CI green → deploy → cek <specific observable>
|
||||
|
||||
## Notes (opsional)
|
||||
|
||||
<Catatan tambahan: risiko, fallback, future work, atau hal yang sengaja di-scope-out.>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,56 @@
|
||||
# Spec: Simpan pesan NSFW tanpa analisis AI (Request 1)
|
||||
|
||||
Date: 2026-08-30
|
||||
|
||||
## Goal
|
||||
Saat ini GMW **skip capture** untuk pesan di channel age-restricted/NSFW
|
||||
(`messageCapture.ts` baris 291 & 310 memanggil `isAgeRestrictedMessage` lalu
|
||||
`return`). User ingin pesan NSFW tetap **disimpan** ke database (jadi terlihat
|
||||
di dashboard), tetapi **tidak dianalisis AI** (tidak dipanggil LLM).
|
||||
|
||||
## Behavior target
|
||||
1. Pesan NSFW/age-restricted di `messageCreate` → DISIMPAN (capture normal).
|
||||
2. Pesan NSFW di `messageUpdate`/`messageDelete` → tetap diproses seperti pesan
|
||||
biasa (edit/deleted state tercatat).
|
||||
3. AI analysis TIDAK berjalan untuk pesan NSFW. Sudah ada jalur yang benar:
|
||||
`queueMessageAnalysis` → `isAgeRestrictedMessage(message)` →
|
||||
`buildAgeRestrictedSkipResult()` yang menulis `status=clean` + flag
|
||||
`age_restricted` + `action=none` TANPA memanggil LLM. Jalur ini sudah ada dan
|
||||
dipakai di `aiAnalyzer.queueMessageAnalysis` (baris 72-84) dan
|
||||
`batchProcessor.skipAgeRestrictedMessages`. Jadi tinggal membuka capture.
|
||||
4. Konten NSFW TIDAK masuk ke Qdrant public archive (semantic search publik).
|
||||
`archiveMessageEmbedded` harus di-guard untuk pesan age-restricted.
|
||||
|
||||
## Files touched
|
||||
- `services/discord-gateway/src/modules/message-capture/messageCapture.ts`
|
||||
- Hapus guard `isAgeRestrictedMessage` di `messageCreate` (baris 291) dan
|
||||
`messageUpdate` (baris 310). Jangan hapus guard `isExcludedThread`,
|
||||
`isBotExcludedChannel`, `shouldCaptureForAnyTarget`.
|
||||
- `services/discord-gateway/src/modules/message-capture/archiveEmbedder.ts`
|
||||
- `archiveMessageEmbedded` terima flag/cek metadata age-restricted → skip
|
||||
embed untuk NSFW.
|
||||
- Call-site di `messageCapture.ts` `captureMessage` (baris 218) pass isNSFW
|
||||
atau cek dulu.
|
||||
|
||||
## Schema/type changes
|
||||
- TIDAK ada perubahan DB schema. Metadata pesan sudah membawa `channel.nsfw`
|
||||
(`getMessageLocation`). Tidak perlu kolom baru — flag `age_restricted` sudah
|
||||
ditulis ke `ai_moderation_flags` lewat skip-result.
|
||||
|
||||
## Verification
|
||||
- `pnpm typecheck`, `pnpm lint` (biome), `pnpm build` di `services/discord-gateway`.
|
||||
- Unit test: pastikan ada test untuk age-restricted skip (sudah ada
|
||||
`tests/conversationContext.test.ts` ref nsfw; cek apakah ada test sesuai).
|
||||
- Deploy via GHA push; verify pesan NSFW muncul di DB dan `ai_status` = clean
|
||||
dengan flag age_restricted, dan TIDAK ada panggilan LLM (log llm-caller tidak
|
||||
menampilkan id pesan NSFW).
|
||||
|
||||
## Status
|
||||
- Request 1: DONE & DEPLOYED (commit 50967f64, CI green, gateway restart 20:17 WIB).
|
||||
- Request 2 (video): dicatat, belum dikerjakan — lihat section di bawah.
|
||||
|
||||
## Request 2 (video) — NOT in this change
|
||||
Fitur record video kamera/screenshare orang lain. @discordjs/voice hanya
|
||||
mendukung **audio** receive. Video orang lain butuh WebRTC viewer baru
|
||||
(SDP answer, decrypt H264/VP8, decode frame, mux MP4/WebM). Diluar scope
|
||||
changeset ini; dicatat untuk desain lanjutan.
|
||||
File renamed without changes.
@@ -0,0 +1,27 @@
|
||||
# Todo — <judul tugas>
|
||||
|
||||
Status: **IN PROGRESS** | **BLOCKED** | **DONE**
|
||||
Spec: <link ke spec: `docs/specs/YYYY-MM-DD_<slug>-spec.md`>
|
||||
Date: YYYY-MM-DD
|
||||
|
||||
## Task
|
||||
|
||||
> Urutan implementasi sesuai spec. Task harus konkret & verifiable
|
||||
> ("ubah X di file Y"), bukan "fix bug". Satu task dikerjakan pada satu
|
||||
> waktu; ceklis `[x]` segera setelah selesai.
|
||||
|
||||
- [ ] Tulis spec
|
||||
- [ ] Verifikasi facts (baca kode: `file:line`)
|
||||
- [ ] Implementasi tahap 1: <apa + file mana>
|
||||
- [ ] Implementasi tahap 2: <apa + file mana>
|
||||
- [ ] Implementasi tahap 3: <apa + file mana>
|
||||
- [ ] Typecheck: `pnpm typecheck` — clean
|
||||
- [ ] Lint: `pnpm lint` — no errors
|
||||
- [ ] Test: `pnpm test` — all pass
|
||||
- [ ] Build: `pnpm build` — compiles
|
||||
- [ ] Daftar ulang hasil verifikasi di spec
|
||||
- [ ] Commit + push (reference spec di commit message)
|
||||
|
||||
## Notes
|
||||
|
||||
<Blocking issue, temuan yang mengubah approach, atau hal yang perlu dicatat.>
|
||||
@@ -35,9 +35,11 @@
|
||||
|
||||
# ---- Shared build tools ----
|
||||
nodejs = pkgs.nodejs_22;
|
||||
pnpm = pkgs.pnpm.override { nodejs = nodejs; };
|
||||
# Bun for deps/install (replaces pnpm); keeps nodejs for the tsc +
|
||||
# fix-imports.mjs build path (Bun's own bundler is not used for dist).
|
||||
bun = pkgs.bun;
|
||||
|
||||
pnpmInstall = ''
|
||||
bunInstall = ''
|
||||
export HOME=$TMPDIR/home
|
||||
export npm_config_cache=$TMPDIR/npm-cache
|
||||
mkdir -p $npm_config_cache
|
||||
@@ -48,15 +50,9 @@
|
||||
export GIT_SSL_CAINFO=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
|
||||
export NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
|
||||
|
||||
# pnpm uses node-gyp for native addons — provide build tools (kept for
|
||||
# the rare case a prebuilt is unavailable and it falls back to compile).
|
||||
export CPPFLAGS="-I${pkgs.lib.getDev pkgs.openssl}/include"
|
||||
export LDFLAGS="-L${pkgs.lib.getLib pkgs.openssl}/lib"
|
||||
|
||||
pnpm install --no-frozen-lockfile --ignore-scripts 2>&1
|
||||
|
||||
# Build native addons that need compilation
|
||||
pnpm rebuild 2>&1 || true
|
||||
# Build native addons (bun install runs postinstall scripts for
|
||||
# @discordjs/opus / sharp unless trustedDependencies restricts).
|
||||
bun install 2>&1
|
||||
'';
|
||||
|
||||
# Shrink the shipped node_modules to production deps only. The full
|
||||
@@ -74,17 +70,15 @@
|
||||
# Must run AFTER tsc (typescript is a devDep) and after native builds.
|
||||
pruneProd = ''
|
||||
echo "=== Pruning devDependencies (production-only node_modules) ==="
|
||||
pnpm list --prod --depth 999 --parseable 2>/dev/null \
|
||||
| grep -o '\.pnpm/[^/]*' | sort -u > $TMPDIR/prod-pnms.txt
|
||||
( cd node_modules/.pnpm \
|
||||
&& for d in */; do \
|
||||
d="''${d%/}"; \
|
||||
[ "$d" = "node_modules" ] && continue; \
|
||||
grep -qF ".pnpm/$d" $TMPDIR/prod-pnms.txt || rm -rf "$d"; \
|
||||
done ) || true
|
||||
# Drop symlinks whose .pnpm target was pruned (top-level, scoped dirs,
|
||||
# hoist, .bin — any depth). Mirrors stdenv's noBrokenSymlinks check,
|
||||
# which would otherwise fail the fixupPhase.
|
||||
# bun install's layout: node_modules/<pkg> for prod deps; devDeps are
|
||||
# also present during build (needed for tsc). Keep only what the prod
|
||||
# graph needs: simplest robust approach is `bun install --production`
|
||||
# semantics — but bun keeps the same flat layout; since the Nix build
|
||||
# already ran `bun install` (full, scripts on), prune dev-only top
|
||||
# entries that were only pulled by devDeps (typescript, biome, vitest,
|
||||
# drizzle-kit, tsx, @types/*).
|
||||
find node_modules -maxdepth 2 -type d \( -name 'typescript' -o -name '@biomejs' -o -name 'vitest' -o -name 'drizzle-kit' -o -name 'tsx' -o -name 'esbuild' \) -prune -exec rm -rf {} + 2>/dev/null || true
|
||||
rm -rf node_modules/.bin/tsc node_modules/.bin/vitest node_modules/.bin/biome node_modules/.bin/drizzle-kit 2>/dev/null || true
|
||||
find node_modules -type l ! -exec test -e {} \; -delete 2>/dev/null || true
|
||||
du -sh node_modules
|
||||
'';
|
||||
@@ -96,11 +90,11 @@
|
||||
|
||||
src = ./services/backend;
|
||||
|
||||
nativeBuildInputs = [ nodejs pnpm pkgs.python3 pkgs.gnumake pkgs.gcc pkgs.cacert ];
|
||||
nativeBuildInputs = [ nodejs bun pkgs.python3 pkgs.gnumake pkgs.gcc pkgs.cacert ];
|
||||
|
||||
buildPhase = pnpmInstall + ''
|
||||
buildPhase = bunInstall + ''
|
||||
echo "=== Compiling TypeScript ==="
|
||||
npx tsc 2>&1
|
||||
./node_modules/.bin/tsc 2>&1
|
||||
echo "=== Fixing @/ path aliases + extensionless relative imports for node ESM ==="
|
||||
node scripts/fix-imports.mjs
|
||||
echo "=== Build complete ==="
|
||||
@@ -132,14 +126,17 @@ WRAPPER
|
||||
|
||||
src = ./services/discord-gateway;
|
||||
|
||||
# cmake + rust/cargo were inherited for node-datachannel-style native
|
||||
# deps — that's 9router/omniroute, NOT GMW. GMW's only native deps
|
||||
# are @discordjs/opus + sharp, both PREBUILT (node-pre-gyp / @img
|
||||
# libvips download), so no cmake or rust toolchain is needed.
|
||||
# python3/gnumake/gcc stay as node-gyp fallback for @discordjs/opus.
|
||||
nativeBuildInputs = [
|
||||
nodejs pnpm
|
||||
pkgs.python3 pkgs.gnumake pkgs.gcc pkgs.cmake
|
||||
pkgs.rustc pkgs.cargo
|
||||
nodejs bun
|
||||
pkgs.python3 pkgs.gnumake pkgs.gcc
|
||||
pkgs.pkg-config
|
||||
pkgs.openssl
|
||||
pkgs.openssl.dev
|
||||
pkgs.git # for any FetchContent-based deps during native builds
|
||||
pkgs.cacert
|
||||
];
|
||||
|
||||
@@ -163,22 +160,11 @@ WRAPPER
|
||||
# neither needed nor wanted here. Skip it entirely.
|
||||
dontFixup = true;
|
||||
|
||||
buildPhase = pnpmInstall + ''
|
||||
echo "=== Building native voice deps ==="
|
||||
# pnpm rebuild aborts on the first failing package and runs scripts
|
||||
# from the wrong cwd — build each native dep explicitly with its own
|
||||
# install script. Each failure is tolerated (|| true); the packages
|
||||
# @discordjs/opus ships prebuilt binaries for Node 22 (ABI node-v127,
|
||||
# linux-x64-glibc-2.35) — node-pre-gyp downloads the prebuilt .node
|
||||
# instead of compiling C++ from source. With build_from_source unset
|
||||
# (above), `pnpm rebuild` runs the package's own install script which
|
||||
# fetches the matching prebuilt; it only falls back to a source build
|
||||
# if the download fails. This keeps voice working without a per-build
|
||||
# native compile.
|
||||
buildPhase = bunInstall + ''
|
||||
echo "=== Rebuilding @discordjs/opus (prebuilt download) ==="
|
||||
pnpm rebuild @discordjs/opus 2>&1 || true
|
||||
bun pm rebuild @discordjs/opus 2>&1 || true
|
||||
echo "=== Compiling TypeScript ===="
|
||||
npx tsc 2>&1
|
||||
./node_modules/.bin/tsc 2>&1
|
||||
echo "=== Fixing @/ path aliases + extensionless relative imports for node ESM ==="
|
||||
node scripts/fix-imports.mjs
|
||||
echo "=== Build complete ==="
|
||||
@@ -214,13 +200,13 @@ WRAPPER
|
||||
|
||||
src = frontendSrc;
|
||||
|
||||
nativeBuildInputs = [ nodejs pnpm pkgs.gnumake pkgs.gcc pkgs.cacert ];
|
||||
nativeBuildInputs = [ nodejs bun pkgs.gnumake pkgs.gcc pkgs.cacert ];
|
||||
|
||||
buildPhase = pnpmInstall + ''
|
||||
buildPhase = bunInstall + ''
|
||||
echo "=== Building Next.js SSR (standalone) ==="
|
||||
export NEXT_TELEMETRY_DISABLED=1
|
||||
export GMW_BACKEND_URL=http://127.0.0.1:4001
|
||||
npx next build 2>&1
|
||||
./node_modules/.bin/next build 2>&1
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
@@ -298,13 +284,13 @@ WRAPPER
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = [
|
||||
nodejs pnpm
|
||||
nodejs bun
|
||||
pkgs.python3 pkgs.gnumake pkgs.gcc
|
||||
pkgs.rustc pkgs.cargo
|
||||
pkgs.ffmpeg-headless
|
||||
];
|
||||
shellHook = ''
|
||||
echo "GMW dev shell ready — node $(node --version), pnpm $(pnpm --version)"
|
||||
echo "GMW dev shell ready — node $(node --version), bun $(bun --version)"
|
||||
'';
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
*
|
||||
!.gitkeep
|
||||
!README.md
|
||||
Whitespace-only changes.
@@ -1,15 +0,0 @@
|
||||
# Recordings Directory
|
||||
|
||||
This directory is mounted as a volume in the discord-gateway container.
|
||||
Voice recordings are stored here with the following structure:
|
||||
|
||||
```
|
||||
recordings/
|
||||
├── <user-id>/
|
||||
│ ├── <timestamp>.ogg
|
||||
│ └── <timestamp>.json
|
||||
└── sessions/
|
||||
└── <session-id>.json
|
||||
```
|
||||
|
||||
The directory must be writable by UID 1000 (app user in the container).
|
||||
@@ -0,0 +1,114 @@
|
||||
# Backend Service — Agent Guide
|
||||
|
||||
> **Read `../../AGENTS.md` first.** This file adds backend-specific conventions.
|
||||
|
||||
Backend service: Express HTTP server + WebSocket, serves the GMW dashboard API.
|
||||
|
||||
## Quick reference
|
||||
|
||||
```bash
|
||||
pnpm typecheck # tsc --noEmit
|
||||
pnpm lint # biome check --diagnostic-level=error .
|
||||
pnpm build # tsc
|
||||
pnpm test # vitest run
|
||||
pnpm format # biome format --write .
|
||||
```
|
||||
|
||||
## Architecture (Modular MVC)
|
||||
|
||||
```
|
||||
src/
|
||||
├── shared/ # Infrastructure (no business logic)
|
||||
│ ├── config/index.ts # Zod-validated env
|
||||
│ ├── database/ # Drizzle ORM + pg Pool
|
||||
│ ├── errors/index.ts # AppError hierarchy
|
||||
│ ├── logger/index.ts # pino + createChildLogger()
|
||||
│ ├── middlewares/index.ts # errorHandler, asyncHandler, rateLimit
|
||||
│ └── utils/ # Pagination, messageMapper
|
||||
├── modules/ # Feature modules
|
||||
│ └── <module>/
|
||||
│ ├── <module>.schema.ts # Zod validation schemas
|
||||
│ ├── <module>.repository.ts # DB operations only
|
||||
│ ├── <module>.service.ts # Business logic
|
||||
│ ├── <module>.controller.ts # HTTP handlers
|
||||
│ └── routes/index.ts # Express router
|
||||
├── http/ # app.ts (factory) + server.ts (startup)
|
||||
├── ws/ # WebSocket server + Redis bridge
|
||||
└── index.ts # Entry point
|
||||
```
|
||||
|
||||
## Dependency rules
|
||||
|
||||
- Controller → Service → Repository → Database
|
||||
- No cross-module repo imports (each module owns its data)
|
||||
- No HTTP in Service layer (no req/res)
|
||||
- No DB in Controller layer
|
||||
- Any layer → Config, Logger, Errors
|
||||
|
||||
## API: oRPC + Express
|
||||
|
||||
- **oRPC** (`src/orpc/router.ts` + `src/orpc/ws.ts`): type-safe procedures for frontend
|
||||
- **Express routes** (`src/modules/*/routes/`): REST endpoints under `/api/`
|
||||
- **WebSocket** (`src/ws/`): Redis bridge → broadcast to connected browsers
|
||||
- Never invent endpoints. Match what the frontend calls (`src/lib/api/`).
|
||||
|
||||
## Key modules
|
||||
|
||||
| Module | Purpose | DB tables |
|
||||
|---|---|---|
|
||||
| messages | Store & query Discord messages | `messages`, `ai_moderations`, `ai_moderation_flags` |
|
||||
| moderation | Moderation actions & metrics | `ai_moderations`, `moderation_actions` |
|
||||
| media | Media file management | `media_attachments` |
|
||||
| dashboard | Stats aggregation | Various (read-only) |
|
||||
| knowledge | Channel cultures & glossary browser | `channel_cultures`, `term_glossary_cache` |
|
||||
| chatbot | AI chatbot with tools | `chatbot_history` |
|
||||
| health | Health checks + metrics | Various |
|
||||
| analysis | Text analysis cache | `text_analysis_cache` |
|
||||
| ui-state | Persist UI preferences | `ui_state` |
|
||||
|
||||
## Config (env vars)
|
||||
|
||||
All validated via Zod in `shared/config/index.ts`. Key vars:
|
||||
|
||||
- `WEBSERVER_PORT` (default 4001)
|
||||
- `DATABASE_URL` or individual `DATABASE_HOST/PORT/NAME/USER/PASSWORD`
|
||||
- `REDIS_URL` — for pub/sub with gateway
|
||||
- `MONITOR_GUILD_ID` — primary Discord guild
|
||||
- `ADMIN_PASSWORD` — admin endpoints
|
||||
|
||||
## Data contract with frontend
|
||||
|
||||
The frontend fetches via `src/lib/api/server.ts` (SSR, server-side) and
|
||||
`src/lib/api/client.ts` (browser, same-origin through proxy).
|
||||
|
||||
**Do not change response shapes without updating both sides.** Check
|
||||
`services/frontend/src/lib/types/` for frontend type definitions.
|
||||
|
||||
## Redis channels (inbound from gateway)
|
||||
|
||||
```
|
||||
discord:message:{created,updated,deleted,analyzed}
|
||||
discord:attachment:{created,uploaded}
|
||||
discord:analysis:queue_status
|
||||
discord:reaction:{added,removed}
|
||||
discord:thread:{created,deleted,updated}
|
||||
discord:channel_topic:updated
|
||||
discord:presence:updated
|
||||
discord:guild_member:{added,removed}
|
||||
```
|
||||
|
||||
Canonical names: `src/shared/redis-channels.ts`.
|
||||
|
||||
## Testing
|
||||
|
||||
- Vitest for unit tests
|
||||
- Mock database and external services
|
||||
- Test files: `src/modules/<module>/*.test.ts` or `tests/*.test.ts`
|
||||
- Run: `pnpm test`
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **oRPC vs REST**: check both routers when adding an endpoint
|
||||
- **Redis channel mismatch**: gateway publishes → backend subscribes. Channel
|
||||
names must match exactly (see `redis-channels.ts` in BOTH services)
|
||||
- **DB pool**: backend uses one pool (main thread). Gateway has per-piscina-thread pools.
|
||||
@@ -35,16 +35,6 @@ services/backend/
|
||||
│ │ │ └── routes/
|
||||
│ │ │ └── index.ts
|
||||
│ │ │
|
||||
│ │ ├── media/
|
||||
│ │ │ ├── media.service.ts
|
||||
│ │ │ └── routes/
|
||||
│ │ │ └── index.ts
|
||||
│ │ │
|
||||
│ │ ├── voice/
|
||||
│ │ │ ├── voice.service.ts
|
||||
│ │ │ └── routes/
|
||||
│ │ │ └── index.ts
|
||||
│ │ │
|
||||
│ │ └── health/
|
||||
│ │ ├── health.schema.ts
|
||||
│ │ ├── health.repository.ts
|
||||
@@ -148,8 +138,6 @@ export const messageQuerySchema = z.object({
|
||||
|--------|---------|--------|
|
||||
| **messages** | Text message storage & retrieval | GET /api/messages, GET /api/messages/:channelId |
|
||||
| **analytics** | Moderation statistics & trends | GET /api/analytics/overview, /daily-trend, /hourly-stats |
|
||||
| **media** | Media file management | GET /api/media/list, POST /api/media/upload |
|
||||
| **voice** | Voice recording management | GET /api/voice/recordings, POST /api/voice/connect |
|
||||
| **health** | Service health checks | GET /api/health |
|
||||
|
||||
## Data Flow
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "discord-moderation-backend",
|
||||
"dependencies": {
|
||||
"@orpc/server": "1.15.3",
|
||||
"axios": "^1.20.0",
|
||||
"dotenv": "^18.0.1",
|
||||
"drizzle-orm": "^0.45.3",
|
||||
"express": "^5.2.1",
|
||||
"helmet": "^8.1.0",
|
||||
"ioredis": "^6.0.0",
|
||||
"pg": "^8.23.0",
|
||||
"pino": "^10.3.1",
|
||||
"prom-client": "^15.1.3",
|
||||
"ws": "^8.21.3",
|
||||
"zod": "^4.6.5",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.5.14",
|
||||
"@types/bun": "latest",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^26.6.2",
|
||||
"@types/pg": "^8.23.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "^7.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@biomejs/biome": ["@biomejs/biome@2.5.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.14", "@biomejs/cli-darwin-x64": "2.5.14", "@biomejs/cli-linux-arm64": "2.5.14", "@biomejs/cli-linux-arm64-musl": "2.5.14", "@biomejs/cli-linux-x64": "2.5.14", "@biomejs/cli-linux-x64-musl": "2.5.14", "@biomejs/cli-win32-arm64": "2.5.14", "@biomejs/cli-win32-x64": "2.5.14" }, "bin": { "biome": "bin/biome" } }, "sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrLine truncated
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.14", "", { "os": "win32", "cpu": "x64" }, "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
||||
|
||||
"@ioredis/commands": ["@ioredis/commands@2.0.0", "", {}, "sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
"@orpc/client": ["@orpc/client@1.15.3", "", { "dependencies": { "@orpc/shared": "1.15.3", "@orpc/standard-server": "1.15.3", "@orpc/standard-server-fetch": "1.15.3", "@orpc/standard-server-peer": "1.15.3" } }, "sha512-ceMe1zGG0uTJr6RfB8TWLJU54DJe3dULkn1A8AUVUDJ4eS+JqMkMmG8zJdEXrkvm4Dwh8majVCpWfCwKuq57Pg=="],
|
||||
|
||||
"@orpc/contract": ["@orpc/contract@1.15.3", "", { "dependencies": { "@orpc/client": "1.15.3", "@orpc/shared": "1.15.3", "@standard-schema/spec": "1.1.0", "openapi-types": "12.1.3" } }, "sha512-ta7LFMMua24obEaYTQerKaR9oGV17mZJ/w0TggJGbBKOsQ5IXxZsDFRh4ugIGFP9KCNPK4XqrENe6SUmc4CEfA=="],
|
||||
|
||||
"@orpc/interop": ["@orpc/interop@1.15.3", "", {}, "sha512-E24sWenXmAy84vTgHsnYsCWT1dciKIq+zEa0Ip3xGHFiz4Sx9s/AA/oNAoKLkM05Q7onH0oMlV2EiImBqa88eQ=="],
|
||||
|
||||
"@orpc/server": ["@orpc/server@1.15.3", "", { "dependencies": { "@orpc/client": "1.15.3", "@orpc/contract": "1.15.3", "@orpc/interop": "1.15.3", "@orpc/shared": "1.15.3", "@orpc/standard-server": "1.15.3", "@orpc/standard-server-aws-lambda": "1.15.3", "@orpc/standard-server-fastify": "1.15.3", "@orpc/standard-server-fetch": "1.15.3", "@orpc/standard-server-node": "1.15.3", "@orpc/standard-server-peer": "1.15.3", "cookie": "1.1.1" }, "optionalDependencies": { "ws": "8.21.3" } }, "sha512-V/ZJLine truncated
|
||||
|
||||
"@orpc/shared": ["@orpc/shared@1.15.3", "", { "dependencies": { "radash": "12.1.1", "type-fest": "5.10.0" }, "optionalDependencies": { "@opentelemetry/api": "1.9.1" } }, "sha512-b8Y0Zt+OhxOJP5Fp7seoPhmSb5Pej8qKqzRxsSbYVb2C88E3XktWaTJVD9fC6lRXED8wDvy4SYag6XSKALwDbg=="],
|
||||
|
||||
"@orpc/standard-server": ["@orpc/standard-server@1.15.3", "", { "dependencies": { "@orpc/shared": "1.15.3" } }, "sha512-ssLeP7EjtBw1k+7jkksnon7CkldSZPG9aBcUT9F46fvPX74ysIcX5swOuek8vS6WazEjd5NPkXYH3OxIpFdi6g=="],
|
||||
|
||||
"@orpc/standard-server-aws-lambda": ["@orpc/standard-server-aws-lambda@1.15.3", "", { "dependencies": { "@orpc/shared": "1.15.3", "@orpc/standard-server": "1.15.3", "@orpc/standard-server-fetch": "1.15.3", "@orpc/standard-server-node": "1.15.3" } }, "sha512-m/0+x/h+T+0MhBn4oFTssvr9Nvf/GZe9DPHqHlY0739I6MAVUOsHbaVV57YAR7Z0NZvvnjJ6IgmWYNg4IO0IOQ=="],
|
||||
|
||||
"@orpc/standard-server-fastify": ["@orpc/standard-server-fastify@1.15.3", "", { "dependencies": { "@orpc/shared": "1.15.3", "@orpc/standard-server": "1.15.3", "@orpc/standard-server-node": "1.15.3" } }, "sha512-2232+PBDJA5E+aex9hc8WHDdRBkYV+o5tYYnebbrDTNmhYEjiD9sp6z9je3RQvC9ap0yuXLHI38IYirNey5A9A=="],
|
||||
|
||||
"@orpc/standard-server-fetch": ["@orpc/standard-server-fetch@1.15.3", "", { "dependencies": { "@orpc/shared": "1.15.3", "@orpc/standard-server": "1.15.3" } }, "sha512-m1S5pQuDngbYc+FX7Y6wekENjUphSPbTO0dEnfK7KBeZSH9mteh/0gyMS0YynXBU8AA7Yn3qTckoPzdRvgD8oA=="],
|
||||
|
||||
"@orpc/standard-server-node": ["@orpc/standard-server-node@1.15.3", "", { "dependencies": { "@orpc/shared": "1.15.3", "@orpc/standard-server": "1.15.3", "@orpc/standard-server-fetch": "1.15.3" } }, "sha512-jxsBRszSWG2je6cpZLJzWeTK/iz2xJHGYthsOuexFmmWWB9WqALwx2gjbOMi9zythFiU/AqD1pgTH2rnPYRU3w=="],
|
||||
|
||||
"@orpc/standard-server-peer": ["@orpc/standard-server-peer@1.15.3", "", { "dependencies": { "@orpc/shared": "1.15.3", "@orpc/standard-server": "1.15.3" } }, "sha512-1/vKuHc/dibDVKyT6PkzZ0V2T9TchQBz9lw7avCc0oGmIqNKqCsF3cWxMh1FEH8orI8VWu1o54s5uq63Px5rvw=="],
|
||||
|
||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "3.4.38", "@types/node": "26.6.2" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.4.2", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="],
|
||||
|
||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "26.6.2" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
|
||||
"@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "1.19.6", "@types/express-serve-static-core": "5.1.2", "@types/serve-static": "2.2.0" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
|
||||
|
||||
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.2", "", { "dependencies": { "@types/node": "26.6.2", "@types/qs": "6.15.1", "@types/range-parser": "1.2.7", "@types/send": "1.2.1" } }, "sha512-d3KvEXBSo/lOAMc2u6fkyDHBvetBHeqD7wm/AcXfLpSOQwlmG9D/aQ0SFswVjv05p7ullQS7Mjohj6/VdbZuTg=="],
|
||||
|
||||
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||
|
||||
"@types/node": ["@types/node@26.6.2", "", { "dependencies": { "undici-types": "8.9.0" } }, "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g=="],
|
||||
|
||||
"@types/pg": ["@types/pg@8.23.1", "", { "dependencies": { "@types/node": "26.6.2", "pg-protocol": "1.16.0", "pg-types": "2.2.0" } }, "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="],
|
||||
|
||||
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
|
||||
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "26.6.2" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
|
||||
|
||||
"@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "2.0.5", "@types/node": "26.6.2" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "26.6.2" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
|
||||
|
||||
"@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
|
||||
|
||||
"@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="],
|
||||
|
||||
"@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="],
|
||||
|
||||
"@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="],
|
||||
|
||||
"@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="],
|
||||
|
||||
"@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="],
|
||||
|
||||
"@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="],
|
||||
|
||||
"@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="],
|
||||
|
||||
"@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="],
|
||||
|
||||
"@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="],
|
||||
|
||||
"@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="],
|
||||
|
||||
"@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="],
|
||||
|
||||
"@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="],
|
||||
|
||||
"@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="],
|
||||
|
||||
"@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="],
|
||||
|
||||
"@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="],
|
||||
|
||||
"@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="],
|
||||
|
||||
"@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="],
|
||||
|
||||
"@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "3.0.2", "negotiator": "1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4.4.3" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
||||
|
||||
"axios": ["axios@1.20.0", "", { "dependencies": { "follow-redirects": "1.16.0", "form-data": "4.0.6", "https-proxy-agent": "5.0.1", "proxy-from-env": "2.1.0" } }, "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg=="],
|
||||
|
||||
"bintrees": ["bintrees@1.0.2", "", {}, "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="],
|
||||
|
||||
"body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "3.1.2", "content-type": "2.0.0", "debug": "4.4.3", "http-errors": "2.0.1", "iconv-lite": "0.7.3", "on-finished": "2.4.1", "qs": "6.15.3", "raw-body": "3.0.2", "type-is": "2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "1.3.0", "function-bind": "1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "get-intrinsic": "1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dotenv": ["dotenv@18.0.1", "", { "bin": { "dotenv": "dist/index.cjs" } }, "sha512-0eR4m4D/jH5eaI3evo2ZqqMii5mrTCR12v12VChdie6O1gPHs7XAjzTep0CWc1Bd+oNx3vVfbi734dK4zghigw=="],
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@0.45.3", "", { "optionalDependencies": { "@opentelemetry/api": "1.9.1", "@types/pg": "8.23.1", "pg": "8.23.0" } }, "sha512-CAloER21cDdcgZ1OmjrZX82EeRNsdP+y0onX/eyoYDaYzki/adrAa91lzM5jWh7zZPK4qUbw/6LGv9J+A+uizA=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "es-errors": "1.3.0", "gopd": "1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "1.3.0", "get-intrinsic": "1.3.0", "has-tostringtag": "1.0.2", "hasown": "2.0.4" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64elLine truncated
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "2.0.0", "body-parser": "2.3.0", "content-disposition": "1.1.0", "content-type": "1.0.5", "cookie": "0.7.2", "cookie-signature": "1.2.2", "debug": "4.4.3", "depd": "2.0.0", "encodeurl": "2.0.0", "escape-html": "1.0.3", "etag": "1.8.1", "finalhandler": "2.1.1", "fresh": "2.0.0", "http-errors": "2.0.1", "merge-descriptors": "2.0.0", "mime-types": "3.0.2", "on-finished": "2.4.1", "once": "1.4.0", "parseurl": "1.3.3", "proxy-addr":Line truncated
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "4.4.3", "encodeurl": "2.0.0", "escape-html": "1.0.3", "on-finished": "2.4.1", "parseurl": "1.3.3", "statuses": "2.0.2" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.16.0", "", { "optionalDependencies": { "debug": "4.4.3" } }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
|
||||
|
||||
"form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "0.4.0", "combined-stream": "1.0.8", "es-set-tostringtag": "2.1.0", "hasown": "2.0.4", "mime-types": "2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "es-define-property": "1.0.1", "es-errors": "1.3.0", "es-object-atoms": "1.1.2", "function-bind": "1.1.2", "get-proto": "1.0.1", "gopd": "1.2.0", "has-symbols": "1.1.0", "hasown": "2.0.4", "math-intrinsics": "1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "1.0.1", "es-object-atoms": "1.1.2" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "1.1.0" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
|
||||
"helmet": ["helmet@8.3.0", "", {}, "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.2", "toidentifier": "1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6.0.2", "debug": "4.4.3" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": "2.1.2" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ioredis": ["ioredis@6.0.0", "", { "dependencies": { "@ioredis/commands": "2.0.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "standard-as-callback": "2.1.0" } }, "sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1.0.2" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
||||
|
||||
"pg": ["pg@8.23.0", "", { "dependencies": { "pg-connection-string": "2.14.0", "pg-pool": "3.14.0", "pg-protocol": "1.16.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "1.4.0" } }, "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg=="],
|
||||
|
||||
"pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="],
|
||||
|
||||
"pg-connection-string": ["pg-connection-string@2.14.0", "", {}, "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg=="],
|
||||
|
||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||
|
||||
"pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": "8.23.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="],
|
||||
|
||||
"pg-protocol": ["pg-protocol@1.16.0", "", {}, "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg=="],
|
||||
|
||||
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "2.0.0", "postgres-bytea": "1.0.1", "postgres-date": "1.0.7", "postgres-interval": "1.2.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||
|
||||
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "4.2.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||
|
||||
"pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "0.4.0", "atomic-sleep": "1.0.0", "on-exit-leak-free": "2.1.2", "pino-abstract-transport": "3.0.0", "pino-std-serializers": "7.1.0", "process-warning": "5.1.0", "quick-format-unescaped": "4.0.4", "real-require": "0.2.0", "safe-stable-stringify": "2.5.0", "sonic-boom": "4.2.1", "thread-stream": "4.2.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1Line truncated
|
||||
|
||||
"pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "4.2.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
|
||||
|
||||
"pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
|
||||
|
||||
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||
|
||||
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
||||
|
||||
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||
|
||||
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "4.0.2" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||
|
||||
"process-warning": ["process-warning@5.1.0", "", {}, "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw=="],
|
||||
|
||||
"prom-client": ["prom-client@15.1.3", "", { "dependencies": { "@opentelemetry/api": "1.9.1", "tdigest": "0.1.2" } }, "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
|
||||
|
||||
"qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "1.0.1", "side-channel": "1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="],
|
||||
|
||||
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
|
||||
|
||||
"radash": ["radash@12.1.1", "", {}, "sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA=="],
|
||||
|
||||
"range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.1", "iconv-lite": "0.7.3", "unpipe": "1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "4.4.3", "depd": "2.0.0", "is-promise": "4.0.0", "parseurl": "1.3.3", "path-to-regexp": "8.4.2" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "4.4.3", "encodeurl": "2.0.0", "escape-html": "1.0.3", "etag": "1.8.1", "fresh": "2.0.0", "http-errors": "2.0.1", "mime-types": "3.0.2", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "1.3.0", "statuses": "2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "2.0.0", "escape-html": "1.0.3", "parseurl": "1.3.3", "send": "1.2.1" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "1.3.0", "object-inspect": "1.13.4", "side-channel-list": "1.0.1", "side-channel-map": "1.0.1", "side-channel-weakmap": "1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "1.3.0", "object-inspect": "1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "get-intrinsic": "1.3.0", "object-inspect": "1.13.4" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "get-intrinsic": "1.3.0", "object-inspect": "1.13.4", "side-channel-map": "1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="],
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
||||
|
||||
"tdigest": ["tdigest@0.1.2", "", { "dependencies": { "bintrees": "1.0.2" } }, "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA=="],
|
||||
|
||||
"thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"tsx": ["tsx@4.23.15", "", { "dependencies": { "esbuild": "0.28.2" }, "optionalDependencies": { "fsevents": "2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw=="],
|
||||
|
||||
"type-fest": ["type-fest@5.10.0", "", { "dependencies": { "tagged-tag": "1.0.0" } }, "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA=="],
|
||||
|
||||
"type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "2.0.0", "media-typer": "1.1.1", "mime-types": "3.0.2" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="],
|
||||
|
||||
"typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@Line truncated
|
||||
|
||||
"undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"ws": ["ws@8.21.3", "", {}, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="],
|
||||
|
||||
"body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
||||
|
||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="],
|
||||
|
||||
"type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="],
|
||||
|
||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[test]
|
||||
preload = ["./tests/setup-env.ts"]
|
||||
@@ -2,23 +2,29 @@
|
||||
"name": "discord-moderation-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend service for Discord moderation monitoring",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"packageManager": "bun@1.3.14",
|
||||
"engines": {
|
||||
"node": ">=22.12.0",
|
||||
"pnpm": ">=9.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"start": "node dist/index.js",
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "biome check --diagnostic-level=error .",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"format": "biome format --write .",
|
||||
"test": "vitest run"
|
||||
"lint": "biome check --diagnostic-level=error .",
|
||||
"start": "node dist/index.js",
|
||||
"test": "bun test tests/",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:e2e": "bun test src/e2e.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordjs/voice": "^0.19.2",
|
||||
"@orpc/server": "1.15.0",
|
||||
"axios": "^1.16.1",
|
||||
"dotenv": "^17.4.2",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"@orpc/server": "1.15.3",
|
||||
"axios": "^1.20.0",
|
||||
"dotenv": "^18.0.1",
|
||||
"drizzle-orm": "^0.45.3",
|
||||
"express": "^5.2.1",
|
||||
"helmet": "^8.1.0",
|
||||
"ioredis": "^6.0.0",
|
||||
@@ -26,16 +32,16 @@
|
||||
"pino": "^10.3.1",
|
||||
"prom-client": "^15.1.3",
|
||||
"ws": "^8.21.3",
|
||||
"zod": "^4.4.3"
|
||||
"zod": "^4.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "latest",
|
||||
"@biomejs/biome": "^2.5.14",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/node": "^26.6.2",
|
||||
"@types/pg": "^8.23.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tsx": "^4.23.12",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "^7.0.2",
|
||||
"vitest": "latest"
|
||||
"@types/bun": "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-2933
File diff suppressed because it is too large.
Load diff
@@ -6,7 +6,13 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
const BASE = process.env.API_BASE ?? "http://localhost:4001/api";
|
||||
|
||||
async function api(path: string, init?: RequestInit) {
|
||||
/** Result of a JSON API call: status + parsed body (or null for 204/empty). */
|
||||
interface ApiResult {
|
||||
status: number;
|
||||
body: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
async function api(path: string, init?: RequestInit): Promise<ApiResult> {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
...init,
|
||||
headers: { "Content-Type": "application/json", ...init?.headers },
|
||||
@@ -23,7 +29,8 @@ describe("API Health", () => {
|
||||
});
|
||||
|
||||
it("GET /metrics returns prometheus text", async () => {
|
||||
const res = await fetch(`${BASE.replace("/api", "")}/api/metrics`);
|
||||
const base = BASE.endsWith("/api") ? BASE.slice(0, -4) : BASE;
|
||||
const res = await fetch(`${base}/api/metrics`);
|
||||
expect(res.status).toBe(200);
|
||||
const text = await res.text();
|
||||
expect(text).toContain("nodejs");
|
||||
@@ -41,20 +48,6 @@ describe("API Dashboard", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("API Recordings", () => {
|
||||
it("GET /recordings returns items with pagination", async () => {
|
||||
const { status, body } = await api("/recordings?limit=5");
|
||||
expect(status).toBe(200);
|
||||
expect(body).toHaveProperty("items");
|
||||
expect(Array.isArray(body.items)).toBe(true);
|
||||
if (body.items.length > 0) {
|
||||
expect(body.items[0]).toHaveProperty("id");
|
||||
expect(body.items[0]).toHaveProperty("username");
|
||||
expect(body.items[0]).toHaveProperty("created_at");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("API Config", () => {
|
||||
it("GET /config returns 200", async () => {
|
||||
const { status } = await api("/config");
|
||||
@@ -62,13 +55,6 @@ describe("API Config", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("API Voice", () => {
|
||||
it("GET /guilds returns 200", async () => {
|
||||
const { status } = await api("/guilds");
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("API Negative", () => {
|
||||
it("GET /nonexistent returns 404", async () => {
|
||||
const { status } = await api("/nonexistent");
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const searchQuerySchema = z.object({
|
||||
q: z.string().default(""),
|
||||
channelId: z.string().optional(),
|
||||
guildId: z.string().optional(),
|
||||
limit: z.coerce.number().int().positive().max(100).default(20),
|
||||
});
|
||||
|
||||
export type SearchQuery = z.infer<typeof searchQuerySchema>;
|
||||
@@ -26,13 +26,25 @@ class ChatbotService {
|
||||
// longer bake server stats into the prompt — the model must pull current
|
||||
// data via tools (see buildSystemPrompt), so it always answers from live
|
||||
// numbers instead of a stale snapshot.
|
||||
const scope = {
|
||||
//
|
||||
// If the current request doesn't carry a guild/channel scope (e.g. a
|
||||
// cross-server dashboard surface), fall back to the most recent scope the
|
||||
// user was chatting in from history, so the agent doesn't drift to the
|
||||
// wrong server between turns.
|
||||
const explicitScope = {
|
||||
guildId: context?.guildId,
|
||||
channelId: context?.channelId,
|
||||
};
|
||||
const scope =
|
||||
explicitScope.guildId || explicitScope.channelId
|
||||
? explicitScope
|
||||
: (recentContext.lastScope ?? {
|
||||
guildId: undefined,
|
||||
channelId: undefined,
|
||||
});
|
||||
|
||||
const systemPrompt = this.buildSystemPrompt(scope);
|
||||
const conversationHistory = this.buildHistoryMessages(recentContext);
|
||||
const conversationHistory = this.buildHistoryMessages(recentContext.turns);
|
||||
const llmResponse = await this.callLLM(
|
||||
systemPrompt,
|
||||
conversationHistory,
|
||||
@@ -61,14 +73,28 @@ class ChatbotService {
|
||||
await chatbotRepository.clearChatHistory(userId);
|
||||
}
|
||||
|
||||
private async getRecentConversationContext(
|
||||
userId: string,
|
||||
): Promise<string[]> {
|
||||
const history = await chatbotRepository.getChatHistory(userId, 3);
|
||||
return history.flatMap((row) => [
|
||||
`User: ${row.user_message}`,
|
||||
`Bot: ${row.bot_response}`,
|
||||
]);
|
||||
private async getRecentConversationContext(userId: string): Promise<{
|
||||
turns: string[];
|
||||
lastScope: { guildId?: string; channelId?: string } | null;
|
||||
}> {
|
||||
const history = await chatbotRepository.getChatHistory(userId, 8);
|
||||
const turns: string[] = [];
|
||||
let lastScope: { guildId?: string; channelId?: string } | null = null;
|
||||
for (const row of history) {
|
||||
turns.push(`User: ${row.user_message}`, `Bot: ${row.bot_response}`);
|
||||
// Capture the most recent scope this user was chatting in, so the
|
||||
// agent keeps server context across turns even if the current request
|
||||
// doesn't carry one.
|
||||
const g = row.context?.guildId;
|
||||
const c = row.context?.channelId;
|
||||
if (g || c) {
|
||||
lastScope = {
|
||||
guildId: g ?? undefined,
|
||||
channelId: c ?? undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { turns, lastScope };
|
||||
}
|
||||
|
||||
private buildSystemPrompt(scope: {
|
||||
@@ -82,11 +108,27 @@ class ChatbotService {
|
||||
|
||||
${scopeLine}
|
||||
|
||||
TOOLS YANG TERSEDIA (panggil saat user tanya soal data server):
|
||||
- get_server_stats → statistik server: total pesan, user aktif, flagged/warn/clean
|
||||
- get_top_channels → channel paling aktif (jumlah pesan terbanyak)
|
||||
- get_recent_activity → pesan terbaru (siapa, channel mana, jam berapa, isinya)
|
||||
- get_top_flagged → pesan yang di-flag AI (alasan, severity, analysis)
|
||||
- search_messages → cari pesan berdasarkan kata kunci (LIKE search)
|
||||
- get_user_messages → pesan terbaru dari satu user tertentu
|
||||
- get_user_profile → profil AI dari seorang user (pola perilaku, gaya bicara)
|
||||
- get_channel_culture → norma/slang dari suatu channel
|
||||
- get_message_detail → 1 pesan lengkap beserta hasil analisis AI-nya
|
||||
- get_message_reviews → antrean review moderasi manual
|
||||
- get_voice_recordings → rekaman suara terbaru
|
||||
- get_moderation_timeline → tren harian: total vs flagged vs warn vs clean
|
||||
- get_corrections → riwayat koreksi false-positive AI
|
||||
|
||||
ATURAN PENTING — JANGAN PAKAI KONTEKS STATIS:
|
||||
- Kamu TIDAK punya hafalan soal angka server (jumlah pesan, user aktif, flagged, dll). JANGAN tebak atau karang angka.
|
||||
- Untuk SEMUA pertanyaan soal data server (jumlah pesan, user aktif, channel ramai, aktivitas terbaru, pesan di-flag), WAJIB panggil tool yang sesuai (get_server_stats, get_top_channels, get_recent_activity, get_top_flagged). Jawab HANYA dari hasil tool.
|
||||
- Tool otomatis di-scope ke guild/channel di atas — kalau argumen guildId/channelId kosong, biarkan kosong (sudah otomatis ter-isi). Jangan isi ID yang kamu tebak.
|
||||
- Kalau tool balas error atau kosong, bilang aja data lagi ga ketemu, jangan karang.
|
||||
- Kamu TIDAK punya hafalan soal angka server. JANGAN tebak atau karang angka.
|
||||
- Untuk SEMUA pertanyaan soal data server, WAJIB panggil tool yang sesuai. Jawab HANYA dari hasil tool.
|
||||
- Tool otomatis di-scope ke guild/channel — kalau argumen kosong, biarkan kosong. Jangan isi ID tebakan.
|
||||
- Kalau tool balik error atau kosong, bilang aja data lagi ga ketemu, jangan karang.
|
||||
- Boleh panggil banyak tool dalam satu jawaban kalau pertanyaan butuh beberapa data (multi-hop).
|
||||
|
||||
Gaya ngobrol:
|
||||
- Santai, hangat, kayak ngobrol sama temen
|
||||
@@ -127,27 +169,31 @@ Gaya ngobrol:
|
||||
try {
|
||||
const { default: axios } = await import("axios");
|
||||
|
||||
// Gateway tidak handle role system — gabung konteks ke user message.
|
||||
// The system section stays visible to the model as the first user turn.
|
||||
const contextPrefixed = `${systemPrompt}\n\nPertanyaan user: ${userMessage}`;
|
||||
|
||||
// Seed conversation: prior turns + current question.
|
||||
const messages: Array<
|
||||
| { role: "user" | "assistant"; content: string }
|
||||
| {
|
||||
role: "assistant";
|
||||
content: string | null;
|
||||
tool_calls: Array<{
|
||||
id: string;
|
||||
type: "function";
|
||||
function: { name: string; arguments: string };
|
||||
}>;
|
||||
}
|
||||
| { role: "tool"; tool_call_id: string; content: string }
|
||||
> = [...history, { role: "user", content: contextPrefixed }];
|
||||
const scopeHint = scope.guildId
|
||||
? ` (scope: guild ${scope.guildId}${scope.channelId ? `, channel ${scope.channelId}` : ""})`
|
||||
: "";
|
||||
// Local message type that models the shapes the agent loop emits and
|
||||
// accepts: plain system/user/assistant turns and assistant tool-calls +
|
||||
// tool results. role is a union string so history entries typed as
|
||||
// `"user" | "assistant"` don't break the discriminant.
|
||||
type ChatMsg = {
|
||||
role: "system" | "user" | "assistant" | "tool";
|
||||
content?: string | null;
|
||||
tool_call_id?: string;
|
||||
tool_calls?: Array<{
|
||||
id: string;
|
||||
type: "function";
|
||||
function: { name: string; arguments: string };
|
||||
}>;
|
||||
};
|
||||
const messages: ChatMsg[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...history,
|
||||
{ role: "user", content: `${userMessage}${scopeHint}` },
|
||||
];
|
||||
|
||||
// ── Agentic tool loop ─────────────────────────────────────────
|
||||
const MAX_TOOL_ROUNDS = 4;
|
||||
const MAX_TOOL_ROUNDS = 6;
|
||||
for (let round = 0; round <= MAX_TOOL_ROUNDS; round += 1) {
|
||||
const response = await axios.post(
|
||||
`${baseUrl}/chat/completions`,
|
||||
@@ -193,35 +239,44 @@ Gaya ngobrol:
|
||||
);
|
||||
|
||||
if (toolCalls.length > 0) {
|
||||
// Execute each tool, append tool results, continue loop.
|
||||
for (const tc of toolCalls) {
|
||||
messages.push({
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: tc.id,
|
||||
type: "function",
|
||||
function: { name: tc.name, arguments: tc.arguments },
|
||||
},
|
||||
],
|
||||
});
|
||||
// Auto-scope: if the model omitted guildId/channelId, fill them
|
||||
// from the request scope so tools query the right server without
|
||||
// the model having to guess IDs.
|
||||
const scopedArgs = { ...tc.args };
|
||||
if (scope.guildId && scopedArgs.guildId == null) {
|
||||
scopedArgs.guildId = scope.guildId;
|
||||
}
|
||||
if (scope.channelId && scopedArgs.channelId == null) {
|
||||
scopedArgs.channelId = scope.channelId;
|
||||
}
|
||||
let result = "";
|
||||
try {
|
||||
result = await executeTool(tc.name, scopedArgs);
|
||||
} catch (e) {
|
||||
result = `Tool error: ${(e as Error).message}`;
|
||||
}
|
||||
// Executor for a single tool call: pushes the assistant tool_call,
|
||||
// runs the tool, returns { tc, result } so results can be appended
|
||||
// in order after all tools execute in parallel.
|
||||
const executions = await Promise.all(
|
||||
toolCalls.map(async (tc) => {
|
||||
messages.push({
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: tc.id,
|
||||
type: "function",
|
||||
function: { name: tc.name, arguments: tc.arguments },
|
||||
},
|
||||
],
|
||||
});
|
||||
// Auto-scope: if the model omitted guildId/channelId, fill them
|
||||
// from the request scope so tools query the right server without
|
||||
// the model having to guess IDs.
|
||||
const scopedArgs = { ...tc.args };
|
||||
if (scope.guildId && scopedArgs.guildId == null) {
|
||||
scopedArgs.guildId = scope.guildId;
|
||||
}
|
||||
if (scope.channelId && scopedArgs.channelId == null) {
|
||||
scopedArgs.channelId = scope.channelId;
|
||||
}
|
||||
let result = "";
|
||||
try {
|
||||
result = await executeTool(tc.name, scopedArgs);
|
||||
} catch (e) {
|
||||
result = `Tool error: ${(e as Error).message}`;
|
||||
}
|
||||
return { tc, result };
|
||||
}),
|
||||
);
|
||||
// Append tool results in the SAME order as the tool_calls so the
|
||||
// API's function-calling contract isn't violated by reordering.
|
||||
for (const { tc, result } of executions) {
|
||||
messages.push({
|
||||
role: "tool",
|
||||
tool_call_id: tc.id,
|
||||
|
||||
@@ -152,7 +152,7 @@ export const tools: ToolDef[] = [
|
||||
function: {
|
||||
name: "get_user_reputation",
|
||||
description:
|
||||
"Ambil skor trust, jumlah infraction, dan streak pesan bersih seorang user dari user_reputations. Untuk 'berapa trust score si A?' / riwayat pelanggaran. Butuh user_id.",
|
||||
"Cek status skor reputasi per-user. CATATAN: fitur trust score per-user telah dihapus dari sistem — tool ini menjawab 'unavailable' dan menyarankan rasio flag vs total sebagai pengganti.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { and, desc, eq, like, sql } from "drizzle-orm";
|
||||
import { createChildLogger } from "@/shared/logger/index";
|
||||
import { getDatabase } from "../../shared/database/index.js";
|
||||
import {
|
||||
pgChannelCulturesTable,
|
||||
@@ -6,7 +7,6 @@ import {
|
||||
pgMessageReviewsTable,
|
||||
pgMessagesTable,
|
||||
pgUserProfilesTable,
|
||||
pgUserReputationsTable,
|
||||
pgVoiceRecordingsTable,
|
||||
} from "../../shared/index.js";
|
||||
|
||||
@@ -20,6 +20,20 @@ import {
|
||||
*/
|
||||
|
||||
export type ToolResult = string;
|
||||
|
||||
const logger = createChildLogger("chatbot.tools");
|
||||
|
||||
/** Saturating counter of chatbot tool execution errors (for observability). */
|
||||
export let toolErrors = 0;
|
||||
const TOOL_ERROR_CAP = 1000;
|
||||
|
||||
function toolExecError(err: unknown, name: string): string {
|
||||
if (toolErrors < TOOL_ERROR_CAP) toolErrors++;
|
||||
const detail = (err as Error)?.message ?? "unknown";
|
||||
logger.warn({ tool: name, error: detail }, "Chatbot tool execution failed");
|
||||
return `Tool ${name} gagal: ${detail}`;
|
||||
}
|
||||
|
||||
/** Executes a tool call against the real DB and returns a readable result. */
|
||||
export async function executeTool(
|
||||
name: string,
|
||||
@@ -91,7 +105,7 @@ export async function executeTool(
|
||||
}
|
||||
} catch (error) {
|
||||
// Best-effort: if a tool fails, return readable error instead of crashing
|
||||
return `Terjadi kesalahan saat ambil data: ${(error as Error).message ?? "unknown"}`;
|
||||
return toolExecError(error, name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,26 +296,18 @@ async function userProfile(userId?: string, guildId?: string): Promise<string> {
|
||||
}
|
||||
|
||||
async function userReputation(
|
||||
userId?: string,
|
||||
guildId?: string,
|
||||
_userId?: string,
|
||||
_guildId?: string,
|
||||
): Promise<string> {
|
||||
const db = getDatabase();
|
||||
if (!userId) return JSON.stringify({ error: "userId wajib" });
|
||||
const conds = [eq(pgUserReputationsTable.user_id, userId)];
|
||||
if (guildId) conds.push(eq(pgUserReputationsTable.guild_id, guildId));
|
||||
const rows = await db
|
||||
.select({
|
||||
user_id: pgUserReputationsTable.user_id,
|
||||
guild_id: pgUserReputationsTable.guild_id,
|
||||
trust_score: pgUserReputationsTable.trust_score,
|
||||
clean_message_streak: pgUserReputationsTable.clean_message_streak,
|
||||
total_infractions: pgUserReputationsTable.total_infractions,
|
||||
last_infraction_at: pgUserReputationsTable.last_infraction_at,
|
||||
})
|
||||
.from(pgUserReputationsTable)
|
||||
.where(and(...conds))
|
||||
.limit(1);
|
||||
return JSON.stringify(rows[0] ?? { error: "reputasi tidak ditemukan" });
|
||||
// The per-user reputation feature (trust scores, streaks, infractions) was
|
||||
// removed from the gateway (migration 0016 drops user_reputations). Return
|
||||
// an honest "unavailable" answer from the derived moderation signals instead
|
||||
// of querying the now-dropped table.
|
||||
return JSON.stringify({
|
||||
available: false,
|
||||
message:
|
||||
"Skor trust/skala reputasi per-user telah dihapus dari sistem. Gunakan rasio pesan ter-flag vs total untuk menilai risiko (lihat dashboard Users).",
|
||||
});
|
||||
}
|
||||
|
||||
async function channelCulture(channelId?: string): Promise<string> {
|
||||
|
||||
@@ -155,6 +155,8 @@ export class DashboardRepository {
|
||||
p.profile_summary,
|
||||
m.total_messages,
|
||||
m.flagged_count,
|
||||
m.clean_count,
|
||||
m.warn_count,
|
||||
m.last_message_at
|
||||
FROM (
|
||||
SELECT
|
||||
@@ -163,6 +165,8 @@ export class DashboardRepository {
|
||||
avatar_url,
|
||||
COUNT(*)::int AS total_messages,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS flagged_count,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'clean')::int AS clean_count,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'warn')::int AS warn_count,
|
||||
MAX(created_at) AS last_message_at
|
||||
FROM ${pgMessagesTable}
|
||||
GROUP BY user_id, username, avatar_url
|
||||
@@ -182,6 +186,8 @@ export class DashboardRepository {
|
||||
profile_summary: r.profile_summary as string | null,
|
||||
total_messages: Number(r.total_messages),
|
||||
flagged_count: Number(r.flagged_count),
|
||||
clean_count: Number(r.clean_count),
|
||||
warn_count: Number(r.warn_count),
|
||||
last_message_at: r.last_message_at ? Number(r.last_message_at) : null,
|
||||
}));
|
||||
|
||||
@@ -429,6 +435,7 @@ export class DashboardRepository {
|
||||
m.total_messages,
|
||||
m.flagged_count,
|
||||
m.clean_count,
|
||||
m.warn_count,
|
||||
p.profile_summary,
|
||||
p.last_analyzed_at
|
||||
FROM (
|
||||
@@ -438,7 +445,8 @@ export class DashboardRepository {
|
||||
avatar_url,
|
||||
COUNT(*)::int AS total_messages,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS flagged_count,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'clean')::int AS clean_count
|
||||
COUNT(*) FILTER (WHERE ai_status = 'clean')::int AS clean_count,
|
||||
COUNT(*) FILTER (WHERE ai_status = 'warn')::int AS warn_count
|
||||
FROM ${pgMessagesTable}
|
||||
WHERE user_id = ${userId}
|
||||
GROUP BY user_id, username, avatar_url
|
||||
@@ -466,6 +474,7 @@ export class DashboardRepository {
|
||||
total_messages: Number(row.total_messages),
|
||||
flagged_count: Number(row.flagged_count),
|
||||
clean_count: Number(row.clean_count),
|
||||
warn_count: Number(row.warn_count),
|
||||
profile_summary: row.profile_summary as string | null,
|
||||
last_analyzed_at: row.last_analyzed_at
|
||||
? Number(row.last_analyzed_at)
|
||||
|
||||
Loaded 50 of 288 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user