refactor(backend): remove auth for public API
Deploy to VPS / deploy (push) Failing after 35s

- Remove auth module (auth.routes.ts, /api/auth/login)
- Remove adminAuth middleware from voice and media routes
- Remove adminAuth() function from shared middlewares
- Remove auth-related e2e test
- Clean up .env.example
This commit is contained in:
asepharyana
2026-07-26 11:33:45 +07:00
parent f9f1313ccd
commit 831fddd1bf
56 changed files with 6 additions and 25239 deletions
@@ -1,437 +0,0 @@
# Aggressive Codebase Cleanup Design
**Date:** 2026-05-13
**Scope:** Biome setup + modularization + unit tests
**Goal:** Production-ready code with strict typing, testability, and maintainability
---
## Overview
Transform the codebase from a monolithic, loosely-typed structure into modular, well-tested, and strictly-typed components. This involves:
1. **Tooling:** Add Biome (linter + formatter) and Vitest (test runner)
2. **Modularization:** Break `recorder.ts` into focused modules (`audioStream`, `decoder`, `segment`, `metadata`)
3. **Typing:** Eliminate all `any` types, use strict interfaces
4. **Testing:** Add unit tests for core logic (decoder rotation, segment management, metadata)
5. **Scripts:** Add `typecheck`, `lint`, `format`, `test` npm scripts
---
## Architecture
### Current State
- `src/recorder.ts` (345 lines): monolithic, handles audio stream, decoder, segment rotation, metadata
- `src/index.ts`: entry point, minimal error handling
- `src/config.ts`, `src/webserver.ts`, `src/player.ts`, etc.: loosely coupled via globals
- No linting, formatting, or tests
### Target State
```
src/
├── index.ts # Entry point (unchanged)
├── config.ts # Config + env validation (enhanced)
├── types.ts # Shared types (new)
├── recorder/
│ ├── index.ts # Main recording orchestrator
│ ├── audioStream.ts # Audio stream subscription & lifecycle
│ ├── decoder.ts # Opus decoder with rotation & error handling
│ ├── segment.ts # Segment lifecycle (open, close, rotate)
│ ├── metadata.ts # Event metadata collection & serialization
│ └── packetFilter.ts # (move from root)
├── webserver.ts # (unchanged)
├── player.ts # (unchanged)
├── mock-crc.ts # (unchanged)
├── muxer.ts # (unchanged)
├── muxer-aup3.ts # (unchanged)
└── packetFilter.ts # (unchanged)
tests/
├── recorder/
│ ├── decoder.test.ts # Decoder rotation, error recovery
│ ├── segment.test.ts # Segment open/close/rotate logic
│ └── metadata.test.ts # Metadata collection & serialization
└── config.test.ts # Env validation
```
---
## Components
### 1. **types.ts** (new)
Centralized type definitions for recorder subsystem.
```typescript
export interface UserMetadata {
userId: string;
username: string;
tag: string;
displayName: string;
avatarUrl: string;
bot: boolean;
roles: Array<{ id: string; name: string; position: number }>;
highestRole: { id: string; name: string; position: number } | null;
joinedTimestamp: number | null;
}
export interface SegmentMetadata {
userId: string;
username: string;
sessionId: string;
sessionStartTime: number;
segmentIndex: number;
startTime: number;
endTime: number;
durationMs: number;
filename: string;
// ... other fields
}
export interface DecoderConfig {
frameSize: number;
channels: number;
rate: number;
}
export interface SegmentState {
index: number;
startTime: number;
endTime: number | null;
filename: string;
jsonFilename: string;
oggStream: any; // prism.opus.OggLogicalBitstream
out: fs.WriteStream;
}
```
### 2. **config.ts** (enhanced)
Strict env validation with typed config object.
```typescript
export interface Config {
verbose: boolean;
recordingsDir: string;
recordingSegmentMs: number;
decoderRotateMs: number;
decoderCooldownMs: number;
}
export function loadConfig(): Config {
const recordingSegmentMsRaw = Number(process.env.RECORDING_SEGMENT_MS ?? 5_000);
const recordingSegmentMs = Number.isFinite(recordingSegmentMsRaw) && recordingSegmentMsRaw > 0
? recordingSegmentMsRaw
: 0;
return {
verbose: process.env.VERBOSE === 'true',
recordingsDir: process.env.RECORDINGS_DIR ?? './recordings',
recordingSegmentMs,
decoderRotateMs: Number(process.env.DECODER_ROTATE_MS ?? 5_000),
decoderCooldownMs: 30_000,
};
}
export const config = loadConfig();
```
### 3. **recorder/decoder.ts** (new)
Isolated decoder lifecycle with rotation and error recovery.
```typescript
export class OpusDecoder {
private decoder: prism.opus.Decoder | null = null;
private disabledUntil = 0;
private createdAt = 0;
private readonly config: DecoderConfig;
private readonly cooldownMs: number;
private readonly rotateMs: number;
private onData: (pcm: Buffer) => void;
constructor(config: DecoderConfig, cooldownMs: number, rotateMs: number, onData: (pcm: Buffer) => void) {
this.config = config;
this.cooldownMs = cooldownMs;
this.rotateMs = rotateMs;
this.onData = onData;
}
create(): prism.opus.Decoder | null {
if (Date.now() < this.disabledUntil) return null;
try {
const d = new prism.opus.Decoder(this.config);
d.on('data', this.onData);
d.on('error', () => this.handleError());
this.createdAt = Date.now();
return d;
} catch (err) {
console.warn('[decoder] Init failed, cooling down:', err);
this.disabledUntil = Date.now() + this.cooldownMs;
return null;
}
}
rotateIfNeeded(): void {
if (!this.decoder || this.rotateMs <= 0) return;
if (Date.now() - this.createdAt < this.rotateMs) return;
this.destroy();
this.decoder = this.create();
}
write(chunk: Buffer): void {
if (!this.decoder) return;
try {
this.decoder.write(chunk);
} catch (err) {
console.warn('[decoder] Write failed, cooling down:', err);
this.handleError();
}
}
private handleError(): void {
this.disabledUntil = Date.now() + this.cooldownMs;
this.destroy();
}
destroy(): void {
if (!this.decoder) return;
this.decoder.removeAllListeners();
this.decoder.destroy();
this.decoder = null;
this.createdAt = 0;
}
}
```
### 4. **recorder/segment.ts** (new)
Segment lifecycle management (open, close, rotate).
```typescript
export class SegmentManager {
private currentSegment: SegmentState | null = null;
private segmentIndex = 0;
private readonly recordingSegmentMs: number;
private readonly userDir: string;
private readonly userId: string;
private readonly sessionId: string;
private readonly sessionStartTime: number;
constructor(userId: string, userDir: string, sessionId: string, sessionStartTime: number, recordingSegmentMs: number) {
this.userId = userId;
this.userDir = userDir;
this.sessionId = sessionId;
this.sessionStartTime = sessionStartTime;
this.recordingSegmentMs = recordingSegmentMs;
}
open(oggPacketStream: NodeJS.ReadableStream): SegmentState {
const index = this.segmentIndex++;
const startTime = Date.now();
const segmentFilename = path.join(this.userDir, `${startTime}.ogg`);
const segmentJsonFilename = path.join(this.userDir, `${startTime}.json`);
const oggStream = new prism.opus.OggLogicalBitstream({
opusHead: new prism.opus.OpusHead({ channelCount: 2, sampleRate: 48000 }),
pageSizeControl: { maxPackets: 10 },
crc: true,
});
const out = fs.createWriteStream(segmentFilename);
oggPacketStream.pipe(oggStream).pipe(out);
const segment: SegmentState = {
index,
startTime,
endTime: null,
filename: segmentFilename,
jsonFilename: segmentJsonFilename,
oggStream,
out,
};
this.currentSegment = segment;
return segment;
}
close(): void {
if (!this.currentSegment) return;
this.currentSegment.endTime = Date.now();
this.currentSegment.oggStream.end();
this.currentSegment = null;
}
rotateIfNeeded(oggPacketStream: NodeJS.ReadableStream): void {
if (!this.currentSegment || this.recordingSegmentMs <= 0) return;
if (Date.now() - this.currentSegment.startTime < this.recordingSegmentMs) return;
this.close();
this.open(oggPacketStream);
}
getCurrent(): SegmentState | null {
return this.currentSegment;
}
}
```
### 5. **recorder/metadata.ts** (new)
User and event metadata collection.
```typescript
export async function collectUserMetadata(
client: Client,
userId: string,
channel: VoiceChannel
): Promise<UserMetadata> {
const user = client.users.cache.get(userId) || await client.users.fetch(userId).catch(() => null);
const member = channel.guild.members.cache.get(userId) || await channel.guild.members.fetch(userId).catch(() => null);
const username = user?.username ?? 'Unknown User';
const avatarUrl = user?.displayAvatarURL({ format: 'png', size: 64 }) ?? 'https://cdn.discordapp.com/embed/avatars/0.png';
const displayName = member?.displayName ?? username;
const roles = (member?.roles.cache
.filter((role) => role.id !== channel.guild.id)
.sort((a, b) => b.position - a.position)
.map((role) => ({ id: role.id, name: role.name, position: role.position })) ?? []) as Array<{ id: string; name: string; position: number }>;
const highestRole = roles.length > 0 ? roles[0] : null;
const joinedTimestamp = member?.joinedTimestamp ?? null;
return {
userId,
username,
tag: user?.tag ?? 'Unknown#0000',
displayName,
avatarUrl,
bot: user?.bot ?? false,
roles,
highestRole,
joinedTimestamp,
};
}
export function createSegmentMetadata(
userMetadata: UserMetadata,
segment: SegmentState,
sessionId: string,
sessionStartTime: number,
recordingSegmentMs: number
): SegmentMetadata {
const endTime = segment.endTime ?? Date.now();
return {
userId: userMetadata.userId,
username: userMetadata.username,
tag: userMetadata.tag,
displayName: userMetadata.displayName,
avatarUrl: userMetadata.avatarUrl,
bot: userMetadata.bot,
roles: userMetadata.roles,
highestRole: userMetadata.highestRole,
joinedTimestamp: userMetadata.joinedTimestamp,
sessionId,
sessionStartTime,
segmentIndex: segment.index,
segmentMs: recordingSegmentMs,
startTime: segment.startTime,
endTime,
durationMs: endTime - segment.startTime,
filename: path.basename(segment.filename),
};
}
```
### 6. **recorder/audioStream.ts** (new)
Audio stream subscription and packet handling.
```typescript
export async function subscribeToAudioStream(
receiver: VoiceReceiver,
userId: string,
onPacket: (chunk: Buffer) => void,
onEnd: () => void,
onError: (err: Error) => void
): Promise<NodeJS.ReadableStream> {
const audioStream = receiver.subscribe(userId, {
end: {
behavior: EndBehaviorType.AfterSilence,
duration: 3000,
},
});
audioStream.on('data', onPacket);
audioStream.on('end', onEnd);
audioStream.on('error', onError);
return audioStream;
}
```
### 7. **recorder/index.ts** (new)
Main orchestrator, replaces current `recorder.ts`.
Coordinates audio stream, decoder, segment, and metadata. Cleaner, testable logic.
---
## Testing Strategy
### Unit Tests (Vitest)
**decoder.test.ts:**
- Decoder creation succeeds with valid config
- Decoder enters cooldown on error
- Decoder rotates after timeout
- Write fails gracefully during cooldown
**segment.test.ts:**
- Segment opens with correct filename
- Segment closes and sets endTime
- Segment rotates when duration exceeded
- Multiple segments tracked correctly
**metadata.test.ts:**
- User metadata collected correctly
- Segment metadata serialized to JSON
- Missing user data handled gracefully
**config.test.ts:**
- Env vars parsed correctly
- Invalid values default safely
- Numeric validation works
### Integration Tests (manual for now)
- Full recording flow: join → speak → record → disconnect
- Decoder error recovery doesn't crash process
- Segment rotation produces correct files
---
## Implementation Order
1. **Setup tooling:** Biome + Vitest + npm scripts
2. **Create types.ts** — shared interfaces
3. **Enhance config.ts** — strict validation
4. **Extract decoder.ts** — isolated, testable
5. **Extract segment.ts** — lifecycle management
6. **Extract metadata.ts** — data collection
7. **Extract audioStream.ts** — stream handling
8. **Rewrite recorder/index.ts** — orchestrator
9. **Write unit tests** — all modules
10. **Update index.ts** — use new recorder module
11. **Remove old recorder.ts**
12. **Verify behavior** — manual test
---
## Success Criteria
- ✅ No `any` types (except necessary prism/discord.js types)
- ✅ All modules < 150 lines
- ✅ Unit tests pass (decoder, segment, metadata, config)
- ✅ Biome lint + format passes
- ✅ Recording behavior identical to before
- ✅ npm scripts: `typecheck`, `lint`, `format`, `test`
- ✅ All files committed with clear messages
---
## Trade-offs
- **More files:** Easier to understand and test, but more to navigate
- **Setup time:** Biome + Vitest + tests add ~2-3 hours, but pay off in maintainability
- **Behavior:** Identical to current; no feature changes
@@ -1,240 +0,0 @@
# Moderation Watcher Expansion Design
**Date:** 2026-05-13
**Status:** Design Phase
**Scope:** Expand Discord bot from voice-only recorder to full moderation watcher capturing text, images, and voice
## Overview
Transform the existing voice recorder bot into a unified moderation watcher that captures:
- **Voice:** Audio from voice channels (existing)
- **Text:** Messages (new/edited/deleted) from all channels and threads
- **Images:** Attachments uploaded to all channels and threads
All data stored in SQLite database. Attachments uploaded to external picser service. Unified dashboard with separate tabs for each content type, filterable by channel/thread.
## Requirements
### Functional
1. **Text Message Capture**
- Capture new messages: content, author, channel, timestamp
- Capture edited messages: original + edited content, edit timestamp
- Capture deleted messages: content, author, deletion timestamp
- Store in database with full metadata
2. **Image/Attachment Capture**
- Detect attachments in messages
- Upload to `https://picser.asepharyana.tech/api/upload`
- Store `raw_commit` URL in database
- Store attachment metadata: filename, size, type, upload timestamp
3. **Voice Recording** (existing, no changes)
- Continue recording voice segments as-is
- Segments already stored in database via muxer queue
4. **Dashboard API**
- `/api/messages?channel=<id>&type=text|image|voice` — Query messages by type and channel
- `/api/channels` — List all monitored channels
- Real-time WebSocket updates: `message_created`, `message_updated`, `message_deleted`, `attachment_uploaded`
5. **Dashboard UI**
- Three tabs: Voice | Text | Images
- Channel/thread filter dropdown
- Display messages/attachments with metadata (author, timestamp, content)
- Real-time updates via WebSocket, polling fallback
### Non-Functional
- **Target Server:** Configured via `MONITOR_GUILD_ID` environment variable
- **Database:** Single SQLite (`.muxer-queue.db`), extended schema
- **Attachment Upload:** Async, non-blocking; store URL when ready
- **Real-time:** WebSocket for live updates, REST polling as fallback
- **Performance:** Index on channel_id, user_id, created_at for fast queries
## Architecture
### Database Schema
**New Tables:**
```sql
-- Text messages
CREATE TABLE messages (
id TEXT PRIMARY KEY,
guild_id TEXT NOT NULL,
channel_id TEXT NOT NULL,
thread_id TEXT,
user_id TEXT NOT NULL,
username TEXT NOT NULL,
avatar_url TEXT,
content TEXT NOT NULL,
edited_content TEXT,
created_at INTEGER NOT NULL,
edited_at INTEGER,
deleted_at INTEGER,
type TEXT NOT NULL DEFAULT 'text', -- 'text', 'edited', 'deleted'
metadata TEXT -- JSON: roles, etc.
);
CREATE INDEX idx_messages_channel ON messages(channel_id);
CREATE INDEX idx_messages_user ON messages(user_id);
CREATE INDEX idx_messages_created ON messages(created_at DESC);
CREATE INDEX idx_messages_thread ON messages(thread_id);
-- Attachments
CREATE TABLE attachments (
id TEXT PRIMARY KEY,
message_id TEXT NOT NULL,
guild_id TEXT NOT NULL,
channel_id TEXT NOT NULL,
user_id TEXT NOT NULL,
filename TEXT NOT NULL,
size INTEGER NOT NULL,
type TEXT NOT NULL, -- MIME type
discord_url TEXT NOT NULL,
uploaded_url TEXT, -- picser raw_commit URL
upload_status TEXT NOT NULL DEFAULT 'pending', -- 'pending', 'uploaded', 'failed'
upload_error TEXT,
created_at INTEGER NOT NULL,
uploaded_at INTEGER,
FOREIGN KEY (message_id) REFERENCES messages(id)
);
CREATE INDEX idx_attachments_channel ON attachments(channel_id);
CREATE INDEX idx_attachments_message ON attachments(message_id);
CREATE INDEX idx_attachments_status ON attachments(upload_status);
```
### Event Handlers
**New Discord Event Listeners:**
1. `messageCreate` — Insert into `messages` table
2. `messageUpdate` — Update `messages` table with edited content + timestamp
3. `messageDelete` — Mark message as deleted (soft delete with `deleted_at`)
4. `messageReactionAdd` — (Optional: track reactions)
**Attachment Processing:**
- On `messageCreate`: Extract attachments, insert into `attachments` table with `upload_status='pending'`
- Async job: Download from Discord URL, upload to picser, update `uploaded_url` and `upload_status`
- If upload fails: Set `upload_status='failed'`, store error message
### API Endpoints
**REST:**
```
GET /api/messages?channel=<id>&type=text|image|voice&limit=50&offset=0
→ Returns paginated messages/attachments
GET /api/channels
→ Returns list of all channels in monitored guild
GET /api/attachments?channel=<id>&limit=50
→ Returns attachments with upload status
```
**WebSocket Events (outbound):**
```json
{
"type": "message_created",
"data": { "id", "channel_id", "user_id", "username", "content", "created_at" }
}
{
"type": "message_updated",
"data": { "id", "edited_content", "edited_at" }
}
{
"type": "message_deleted",
"data": { "id", "deleted_at" }
}
{
"type": "attachment_uploaded",
"data": { "id", "message_id", "filename", "uploaded_url", "created_at" }
}
```
### File Structure
```
src/
├── moderation/
│ ├── messageCapture.ts -- Discord event listeners
│ ├── attachmentUploader.ts -- Upload to picser, manage queue
│ ├── messageStore.ts -- Database operations
│ └── types.ts -- Message/Attachment types
├── webserver.ts -- Add /api/messages, /api/channels endpoints
├── index.ts -- Register message event listeners
└── config.ts -- Add MONITOR_GUILD_ID
```
### Configuration
**New Environment Variables:**
```env
MONITOR_GUILD_ID=<guild-id> # Target server to monitor
PICSER_UPLOAD_URL=https://picser.asepharyana.tech/api/upload
ATTACHMENT_UPLOAD_TIMEOUT_MS=30000 # Upload timeout
ATTACHMENT_MAX_SIZE_MB=100 # Max file size to upload
```
## Implementation Phases
### Phase 1: Database & Core Capture
- Extend SQLite schema (messages, attachments tables)
- Implement message capture handlers (create/edit/delete)
- Add message store functions (insert, update, query)
### Phase 2: Attachment Upload
- Implement picser uploader with retry logic
- Add attachment processing queue
- Store URLs in database
### Phase 3: API & WebSocket
- Add REST endpoints for querying messages/attachments
- Add WebSocket events for real-time updates
- Implement channel listing
### Phase 4: Dashboard UI
- Build frontend with Voice | Text | Images tabs
- Implement channel filter
- Add real-time WebSocket listener + polling fallback
## Error Handling
- **Upload failures:** Retry with exponential backoff, store error in `upload_error` field
- **Database errors:** Log and continue (don't crash bot)
- **Missing attachments:** Handle Discord URL expiry gracefully
- **WebSocket disconnects:** Clients reconnect and poll for missed messages
## Testing
- Unit tests for message store functions (insert, update, query)
- Integration tests for attachment uploader (mock picser API)
- E2E tests for Discord event capture (mock Discord client)
## Success Criteria
- ✅ All text messages captured (new/edited/deleted)
- ✅ All attachments uploaded to picser with URLs stored
- ✅ Dashboard displays all three content types in separate tabs
- ✅ Channel filter works correctly
- ✅ Real-time WebSocket updates working
- ✅ Polling fallback works if WebSocket disconnects
- ✅ No data loss on bot restart
- ✅ Graceful handling of upload failures
## Future Enhancements
- Reaction tracking
- Message search/full-text search
- Moderation actions (flag, delete, mute)
- Export/archive functionality
- Retention policies (auto-delete old data)
@@ -1,91 +0,0 @@
# Web Interactive Voice Connect Design
## Goal
Replace startup auto-connect with web-driven guild and voice channel selection.
## Current Behavior
The bot reads `GUILD_ID` and `VOICE_CHANNEL_ID` from config on startup. When Discord client emits `ready`, `src/index.ts` immediately fetches that guild/channel, joins the voice channel, starts recording, connects the player, then starts the webserver.
## New Behavior
The bot should login to Discord and start the webserver immediately. The web UI should let the user select a guild and voice channel from dropdowns, then connect or disconnect without restarting the bot.
## API
Add HTTP endpoints in `src/webserver.ts`, backed by the Discord client passed from `src/index.ts`.
- `GET /api/status`
- returns `{ ready, connected, activeGuildId, activeChannelId, activeChannelName }`
- `GET /api/guilds`
- returns guilds available in `client.guilds.cache`
- shape: `{ id, name }[]`
- `GET /api/guilds/:guildId/voice-channels`
- fetches guild by id
- returns voice channels only
- shape: `{ id, name }[]`
- `POST /api/connect`
- body: `{ guildId, channelId }`
- stops existing recording/connection if connected
- validates guild exists and channel is `GUILD_VOICE`
- calls `startRecording(client, channel)`
- updates active connection state
- calls `discordPlayer.setConnection(getVoiceConnection(guildId))`
- `POST /api/disconnect`
- stops current recording if connected
- clears active connection state
- pauses player
## Config
`DISCORD_TOKEN` remains required. `GUILD_ID` and `VOICE_CHANNEL_ID` become optional because selection happens in the web UI.
## Frontend
Update `public/index.html` with a small connection panel above current audio controls:
- Guild dropdown
- Channel dropdown
- Join Channel button
- Disconnect button
- Connection status text
Flow:
1. On page load, fetch `/api/status` and `/api/guilds`.
2. When guild changes, fetch `/api/guilds/:guildId/voice-channels`.
3. Join button sends selected guild/channel to `/api/connect`.
4. Disconnect button sends `/api/disconnect`.
5. Existing transmit/listen WebSocket behavior remains unchanged.
## Error Handling
API returns `400` for missing ids or invalid channel type, `404` for missing guild/channel, and `409` if Discord client is not ready. Frontend shows error text in the connection panel.
## Testing
Run:
```bash
bun run test
bun run typecheck
bun run lint
bun run build
```
Manual browser smoke test:
1. Start bot.
2. Open web UI.
3. Confirm guild dropdown loads.
4. Select guild, confirm voice channel dropdown loads.
5. Click Join Channel, confirm status changes and bot joins voice.
6. Click Disconnect, confirm bot leaves voice.
## Self-Review
- No placeholders.
- Scope is focused on web-driven voice connect only.
- Existing WebSocket audio path remains unchanged.
- Config change matches interactive selection requirement.
@@ -1,264 +0,0 @@
# AI Message Flow + React Dashboard Redesign
## Goal
Rebuild the moderation watcher flow so message capture is fast, AI analysis is contextual and reliable, APIs are split by concern, and the dashboard is maintainable. The implementation may fully replace the current static frontend and reorganize backend modules, while preserving existing voice functionality.
## Current Problems
- Message capture, AI queuing, DB access, and WebSocket broadcasting are tightly coupled.
- AI analysis batches depend on array ordering, which can mismatch results when the model returns malformed or partial JSON.
- Pending analysis is polled globally, not grouped by conversation, so context is weak and request efficiency is inconsistent.
- `/api/messages` mixes text/image concerns and uses offset pagination, which gets slower and less stable as rows grow.
- Frontend is a large static HTML file with inline state, API, WebSocket, rendering, and audio code.
- WebSocket broadcast uses untyped `globalThis` hooks across modules.
## Backend Architecture
### Message ingestion
`messageCapture` becomes a narrow ingestion layer:
1. Filter Discord events by guild and author.
2. Normalize message payload and metadata.
3. Upsert message/attachment records.
4. Set or reset `ai_status` to `pending` for new or edited text.
5. Emit typed domain events for WebSocket broadcasting and analysis queueing.
It should not build prompts, manage AI batches, or query unrelated DB state.
### Store/repository layer
`messageStore` becomes the single message/attachment query boundary. It should expose focused functions:
- `upsertMessage`
- `markMessageEdited`
- `markMessageDeleted`
- `listMessages`
- `listReviewMessages`
- `getConversationContext`
- `claimPendingMessagesForChannel`
- `saveAnalysisResults`
- `insertAttachments`
Queries should use cursor pagination based on `(created_at, id)` instead of offset pagination. Common filters: `guildId`, `channelId`, `threadId`, `status`, `userId`, `q`, `limit`, `cursor`.
### Analysis queue
Add an `analysisQueue` module. It owns async AI processing and keeps capture fast.
- Queue key: `thread_id ?? channel_id`.
- Debounce: 13 seconds per key to group nearby messages.
- Batch pending messages by conversation key and token budget.
- Only one or a small fixed number of active LLM requests.
- Backlog worker feeds the same queue; no separate analysis path.
- Edits reset a message to `pending` and enqueue its conversation key.
If the process restarts, pending rows are recovered by a periodic lightweight scanner grouped by conversation key.
### Conversation context builder
Add `conversationContext` module:
- Input: conversation key + target pending messages.
- Fetch context before the first target message, normally 20 prior messages.
- Include target messages and close neighboring messages when within budget.
- Mark target messages explicitly in the prompt.
- Keep context scoped to one channel/thread to avoid irrelevant noise.
### LLM moderation client
Add `llmModerationClient` module:
- Own request shape, timeout, retry, JSON extraction, and validation.
- Prompt returns JSON keyed by `message_id`, not positional arrays.
- Expected response shape:
```json
{
"results": [
{
"message_id": "string",
"status": "clean|warn|flagged",
"flags": ["string"],
"score": 0.0,
"analysis": "Bahasa Indonesia summary + reason + suggested action"
}
]
}
```
- Reject unknown IDs, invalid statuses, invalid scores, and missing target IDs.
- On partial model failure, retry once with smaller batch. If still invalid, mark only affected target messages as `error`.
- Store raw batch request/response in one run record if the DB migration is included; otherwise store compact raw metadata per message.
## API Design
Split API by use case so reads remain fast and obvious.
### Message read APIs
- `GET /api/messages`
- Query: `guildId`, `channelId`, `threadId`, `cursor`, `limit`, `status`, `userId`, `q`.
- Returns: `{ data, nextCursor }`.
- Uses indexed cursor pagination.
- `GET /api/messages/:id`
- Returns one message with attachments and AI analysis.
- `GET /api/review`
- Query: `guildId`, optional `channelId`, `status=warn,flagged,error`, `cursor`, `limit`.
- Optimized for moderator review panel.
- `GET /api/attachments`
- Query: `channelId`, `threadId`, `cursor`, `limit`, `type`.
- Replaces image mode inside `/api/messages`.
### Analysis APIs
- `POST /api/messages/:id/reanalyze`
- Sets message to `pending` and queues its conversation.
- Returns `202 Accepted` with current message status.
- `POST /api/analysis/requeue-pending`
- Admin/manual recovery endpoint for pending/error rows.
- Returns count queued.
- `GET /api/analysis/status`
- Returns queue depth, active requests, last error, and pending counts.
### Discord sync APIs
- `POST /api/backlog-sync`
- Stays async-friendly: starts sync for guild/channel/thread and returns `202` with a job id or immediate summary if small.
- Sync inserts messages, then queues analysis through the same `analysisQueue`.
### Voice/control APIs
Keep existing voice APIs working, but move route registration into route modules:
- `routes/voiceRoutes.ts`
- `routes/messageRoutes.ts`
- `routes/analysisRoutes.ts`
- `routes/syncRoutes.ts`
- `routes/uiStateRoutes.ts`
`webserver.ts` should only create Express/WS server, install middleware, register routes, and start listening.
## WebSocket Design
Replace ad-hoc globals with a typed broadcaster module.
Events:
- `ui_state`
- `user_state`
- `message_created`
- `message_updated`
- `message_deleted`
- `message_analyzed`
- `attachment_created`
- `analysis_queue_status`
Backend modules call broadcaster functions; they do not touch WebSocket clients directly.
## Database Changes
Add or verify indexes:
- messages `(channel_id, created_at, id)`
- messages `(thread_id, created_at, id)`
- messages `(ai_status, created_at, id)`
- messages `(guild_id, ai_status, created_at, id)`
- attachments `(channel_id, created_at, id)`
- attachments `(thread_id, created_at, id)`
Optional but preferred:
- `ai_analysis_runs` table:
- `id`
- `conversation_key`
- `target_message_ids`
- `model`
- `request_tokens_estimate`
- `response_raw`
- `status`
- `error`
- `created_at`
- `completed_at`
This avoids duplicating large raw LLM responses into every message row.
## React/Vite Frontend
Replace static inline dashboard code with a TypeScript React app.
Suggested structure:
- `frontend/src/api/` — typed REST clients
- `frontend/src/ws/` — WebSocket client and event types
- `frontend/src/state/` — small hooks for selected guild/channel, messages, review queue, voice state
- `frontend/src/components/voice/` — existing voice control/audio components
- `frontend/src/components/messages/` — feed, message card, filters, detail drawer
- `frontend/src/components/review/` — needs-review list and analysis status
- `frontend/src/components/layout/` — shell/sidebar/status cards
UI layout:
- Left sidebar: guild, voice channel, text channel/thread, connection state.
- Main area: message feed with filters and load-more cursor pagination.
- Right panel: review queue for `warn`, `flagged`, and `error` messages.
- Detail drawer/modal: message metadata, attachments, AI rationale, raw flags, reanalyze action.
Voice features stay functionally equivalent. Audio capture/playback code can be moved into React hooks but should not be behaviorally rewritten unless needed.
Build integration:
- Add Vite dev/build scripts.
- Express serves the built app from a stable public directory in production.
- During development, either run Vite separately or proxy API/WS to Express.
## Performance Rules
- Message capture must not wait on AI.
- Read APIs use cursor pagination and indexes.
- AI batches are bounded by token estimate and message count.
- UI fetches initial pages, then patches via WebSocket.
- Backlog sync should not block dashboard interactions.
- Avoid storing full raw LLM response per message when a batch table is available.
## Error Handling
- Capture errors log and do not crash Discord client event handlers.
- AI request failures mark target messages `error` with a short reason.
- Invalid LLM JSON triggers retry/split before marking errors.
- API validation returns 400 with structured error code.
- WebSocket reconnect logic stays client-side.
- Manual reanalysis provides recovery for bad AI results.
## Testing
Backend:
- Unit tests for conversation context selection.
- Unit tests for LLM response parser and validation.
- Unit tests for queue batching/debounce behavior.
- Integration tests for message cursor pagination and review filters.
- Existing voice tests remain unchanged.
Frontend:
- Typecheck and Vite build.
- Component-level smoke tests may be added if test tooling is already practical.
- Manual browser verification: channel select, message feed, review panel, WebSocket updates, reanalyze action, voice controls.
## Implementation Scope
This is a full redesign of the message/AI/dashboard path. Voice recording and live audio behavior should be preserved unless a change is required to integrate the React dashboard.
Implementation should proceed incrementally:
1. Backend boundaries and typed broadcaster.
2. Store/query improvements and indexes.
3. Analysis queue/context/client rewrite.
4. Split API routes.
5. React/Vite dashboard.
6. Verification and cleanup of old static dashboard code.
@@ -1,110 +0,0 @@
# Media Music Phase 1 Design
## Goal
Add a first media playback phase focused on play music: users can queue, play, skip, and stop audio sources from the dashboard while preserving the existing Discord voice recorder, browser microphone transmit, and moderation capture flows.
## Scope
Phase 1 implements audio-only playback and queue control. Share screen/video streaming is intentionally reserved for phase 2, but the controller shape should leave room for a later `screen` mode using the already vendored `@dank074/discord-video-stream` APIs seen in `MythEclipse/StreamBot`.
## Recommended Architecture
Create a small media subsystem under `src/media/`:
- `mediaTypes.ts` defines `MediaMode`, `MediaQueueItem`, `MediaState`, and request/response types.
- `mediaQueue.ts` owns in-memory queue operations: add, current, next, remove current, clear, snapshot.
- `mediaResolver.ts` resolves initial supported sources. Phase 1 should support direct HTTP(S) URLs and local file paths. YouTube/search can be added later because it requires adding or wrapping yt-dlp behavior.
- `musicPlayer.ts` converts a media source to Ogg Opus using ffmpeg and feeds the existing `discordPlayer.playStream()`.
- `mediaController.ts` coordinates queue state, voice connection assumptions, play/skip/stop, and WebSocket broadcast state.
The existing `VoiceController` remains the owner of joining/leaving voice channels. Phase 1 does not create a second voice connection path. Music playback requires the bot to already be connected through the existing voice UI or `/api/connect`; otherwise the media route returns `409 VOICE_NOT_CONNECTED`.
## Data Flow
1. Browser submits a source to `/api/media/queue` with `{ source }`.
2. `mediaResolver` validates and resolves the source into `{ source, title, kind }`.
3. `mediaQueue` appends a `MediaQueueItem`.
4. If no item is playing, `mediaController` starts playback of the current queue item.
5. `musicPlayer` spawns ffmpeg and outputs Ogg Opus to `discordPlayer.playStream()`.
6. When playback finishes, the controller removes the completed item and starts the next item.
7. State changes broadcast over the existing moderation broadcaster as a JSON WebSocket event, or via a small media broadcaster wrapper if that keeps types cleaner.
## API Design
Add `src/routes/mediaRoutes.ts` mounted under `/api`:
- `GET /api/media/status` returns `{ playing, current, queue }`.
- `POST /api/media/queue` accepts `{ source: string }`, queues it, and returns the updated state.
- `POST /api/media/skip` skips current item and starts the next if present.
- `POST /api/media/stop` stops playback and clears the queue.
All routes should use `AppError` for boundary validation. Empty source returns `400 MISSING_MEDIA_SOURCE`. No voice connection returns `409 VOICE_NOT_CONNECTED`.
## Dashboard Design
Add a compact Media card to the existing voice tab for phase 1:
- Source input: URL or local path.
- Buttons: Queue/Play, Skip, Stop.
- Current item label and queue list.
Do not add a separate full media tab yet. The voice tab already owns voice channel selection and connection state, so colocating music controls there reduces user confusion.
## Playback Details
Use ffmpeg directly or the existing `src/audio/ffmpegProcess.ts` helper if it already fits. The target stream should be Ogg Opus because `DiscordPlayer.playStream()` currently expects `StreamType.OggOpus`.
Recommended ffmpeg output shape:
- Input: local file or HTTP(S) URL.
- Output format: `ogg`.
- Audio codec: `libopus`.
- Sample rate: `48000`.
- Channels: `2`.
The controller owns an `AbortController` or child process handle so skip/stop can terminate ffmpeg. Stop must also call `discordPlayer.stop()` so the audio player releases the current resource.
## Concurrency Rules
- Only one media item plays at a time.
- Browser microphone transmit and music playback both use `discordPlayer`; phase 1 should disable music start while `isStreaming` is true, or stop browser transmit before playback. Prefer returning `409 BROWSER_STREAM_ACTIVE` to avoid surprising the user.
- Voice recording can continue while music plays because recording uses the receiver pipeline and music uses the player pipeline.
- Skip is serialized: concurrent skip calls should return the same resulting state or reject with `409 MEDIA_SKIP_IN_PROGRESS`.
## Error Handling
- Unsupported source format: `400 UNSUPPORTED_MEDIA_SOURCE`.
- ffmpeg spawn failure: current item becomes failed, playback advances to the next queued item if present.
- ffmpeg runtime failure: log stderr summary, mark item failed, advance queue.
- Stop is idempotent: stopping while idle returns current idle state.
## Tests
Unit tests should cover:
- Queue add/next/remove/clear behavior.
- Resolver accepts HTTP(S) URLs and existing local paths, rejects empty/unsupported input.
- Controller rejects playback when voice is not connected.
- Controller starts next item after completion.
- Skip aborts current playback and advances queue.
- Routes validate payloads and call controller methods.
Manual verification should cover:
- Connect to a voice channel, queue a short audio URL or local file, hear playback in Discord.
- Queue two items, confirm automatic advance.
- Skip moves to the next item.
- Stop clears playback and queue.
- Existing voice recording and text moderation still work after media playback.
## Phase 2 Compatibility
Phase 2 can add `MediaMode = "screen"` and a `screenSharePlayer.ts` using StreamBot's pattern:
- `new Streamer(client)`
- `streamer.joinVoice(guildId, channelId)` only if phase 2 decides to own its own connection path
- `prepareStream(source, videoOptions, signal)`
- `playStream(output, streamer, { type: "go-live" }, signal)`
Phase 1 should not instantiate `Streamer`; it should only reserve type and controller seams so adding screen share later does not rewrite queue/status APIs.
@@ -1,37 +0,0 @@
# Selfbot Performance and Feature Optimization Design
## Goal
Improve `vendor/discord.js-selfbot-v13` and the app's Discord client setup for lower memory use, more stable REST behavior, lower voice hot-path allocation, and better observability while preserving the existing public API used by the bot.
## Scope
This is an aggressive optimization pass. It includes app-level client configuration plus internal vendor patches in REST, voice, and gateway queue handling. Changes must remain compatible with existing imports from `discord.js-selfbot-v13` and the current moderation/voice flows.
## App Runtime Configuration
`src/index.ts` will instantiate `Client` with explicit low-memory options instead of using `new Client()` defaults. Message cache will be reduced or disabled because captured messages are persisted to the database. Sweepers will remove old message/thread cache entries. REST retry/timeouts will remain conservative to avoid bursty backlog sync behavior.
## Vendor REST Improvements
`src/rest/APIRequest.js` currently uses a module-level Undici dispatcher and rebuilds expensive headers per request. The dispatcher will become per REST manager/client so proxy or client-specific settings cannot leak across clients. The `x-super-properties` header will be cached and reused while client properties remain unchanged. `RequestHandler` will add exponential backoff with jitter for network aborts and 5xx retries.
## Vendor Voice Improvements
`PacketHandler` will clean all speaking timeouts during stream destruction. Voice stream cleanup will clear audio and video stream maps reliably. RTP/decrypt hot-path allocation will be reduced where possible without changing emitted packet payloads or stream API behavior.
## Vendor Gateway Queue Improvements
`WebSocketShard` will replace repeated `Array.shift()` dequeue with a cursor-backed queue to avoid O(n) work under high gateway send volume. `send(data, important)` behavior will remain compatible, including priority insertion.
## Observability
Vendor internals will emit or debug useful operational data where it does not create noisy logs by default: REST retry/backoff attempts, voice stream cleanup counts, and gateway queue size/rate-limit state. The app can wire these later if needed.
## Error Handling
REST retry backoff must not bypass existing rate limit handling. Captcha and MFA retry paths keep their current behavior. Voice cleanup must ignore already-closed streams and never throw during disconnect. Gateway queue changes must clear queued state on destroy exactly as before.
## Testing
Run `pnpm run lint`, `pnpm run typecheck`, and `pnpm run test`. If runtime Discord login/voice testing cannot be performed in this environment, report that limitation explicitly and identify the manual test path: login, message capture, backlog sync, voice connect, voice record, disconnect/reconnect.
@@ -1,24 +0,0 @@
# Selfbot Workspace Submodule Design
## Goal
Replace the npm-resolved `discord.js-selfbot-v13` dependency with a custom repository checked into this project as a git submodule and consumed through pnpm workspace resolution.
## Approach
Use `vendor/discord.js-selfbot-v13` as the submodule path. Initialize it from `https://github.com/aiko-chan-ai/discord.js-selfbot-v13.git`, then change the submodule repository `origin` remote to `ssh://git@43.134.105.109:22222/exceed/discord.js-selfbot.git`.
Configure pnpm workspaces so the root project and the vendored package are both workspace packages. Change the root dependency from the npm version range to `workspace:*`, forcing pnpm to resolve `discord.js-selfbot-v13` from the submodule package.
## Files to Change
- `.gitmodules`: track the new submodule path and URL.
- `pnpm-workspace.yaml`: include the root package and `vendor/discord.js-selfbot-v13`.
- `package.json`: change `discord.js-selfbot-v13` to `workspace:*`.
- `pnpm-lock.yaml`: refresh dependency resolution after the workspace change.
## Validation
After the submodule and dependency changes, run `pnpm install` to update the workspace lockfile and links, then run `pnpm run typecheck` to confirm the app still resolves the selfbot package.
If the vendored package requires a build step before TypeScript can resolve it, use that package's own scripts and rerun root validation.
@@ -1,76 +0,0 @@
# Vendor Selfbot Dependency Modernization Design
## Goal
Modernize `/mnt/code/bete/vendor/discord.js-selfbot-v13` aggressively by auditing runtime dependencies and replacing the legacy development toolchain with Biome, matching the root project style.
## Scope
This work targets the vendored `discord.js-selfbot-v13` submodule only, plus root lockfile/workspace updates required for the root app to consume it. The root app behavior and public import surface should remain compatible with the existing `discord.js-selfbot-v13` API.
## Approach
Audit all vendor `dependencies` and `devDependencies` against actual usage in `src`, `typings`, config files, and package scripts. Classify each package as keep, upgrade, remove, or replace. Apply changes aggressively, but only when usage evidence supports the change.
Replace the vendor's ESLint, Prettier, TSLint, and dtslint-based workflow with Biome. Keep TypeScript validation. Keep `tsd` only if the vendor has type assertion tests that `tsc --noEmit` cannot cover.
## Toolchain Design
Vendor scripts should use Biome for linting and formatting:
- `lint`: `biome check . --diagnostic-level=error`
- `format`: `biome format --write .`
- `test:typescript`: `tsc --noEmit` plus `tsd` only if type assertion tests exist
- `test`: run lint and TypeScript validation
Remove deprecated or redundant dev dependencies after scripts no longer reference them:
- `eslint`
- `eslint-config-prettier`
- `eslint-plugin-import`
- `eslint-plugin-prettier`
- `prettier`
- `tslint`
- `dtslint`
Add `@biomejs/biome` to the vendor dev dependencies unless the workspace can reliably use the root Biome package for the vendor scripts.
## Runtime Dependency Design
Runtime dependencies are reviewed one by one. Candidate packages include:
- `find-process`
- `tree-kill`
- `prism-media`
- `werift-rtp`
- `fetch-cookie`
- `tough-cookie`
- `qrcode`
- `otplib`
- `ws`
- `undici`
- `discord-api-types`
- `@discordjs/builders`
- `@discordjs/collection`
- `@sapphire/async-queue`
- `@sapphire/shapeshift`
For each dependency, search source usage before changing it. Remove unused packages. Upgrade packages that remain used. Replace packages when Node 20+ or a smaller maintained package covers the same use case without changing public behavior.
## Validation
Validation must run in both vendor and root contexts:
1. Vendor dependency install/update.
2. Vendor lint with Biome.
3. Vendor TypeScript/type validation.
4. Root `pnpm install` to refresh workspace lockfile.
5. Root `pnpm run typecheck`.
6. Root `pnpm run lint`.
7. Import smoke check from the root app to ensure `discord.js-selfbot-v13` still resolves through the workspace link.
## Stop Rules
Stop and ask before making a change that would intentionally alter the public `discord.js-selfbot-v13` API, require ESM-only migration for the library entrypoint, or remove a runtime feature that the root app could use.
If a dependency upgrade requires broad internal rewrites, document the blocker and present options instead of forcing a risky migration.
@@ -1,71 +0,0 @@
# Media Echo Fix and YouTube Screenshare Design
## Context
Media playback currently uses the same `DiscordPlayer` instance as the browser audio bridge. The browser bridge is started during webserver startup and subscribes the shared player to the active voice connection. Music playback also uses that player. This shared ownership can let the bridge interfere with media playback and contribute to voice audio being reflected back during playback.
The project already includes `@dank074/discord-video-stream`, which supports Discord Go Live video streaming from a direct media URL or readable stream.
## Goals
- Prevent voice audio from being reflected back while music/media playback is active.
- Keep normal music playback behavior for existing `/api/media/queue` users.
- Add a YouTube screenshare path that streams video through Discord Go Live.
- Fail clearly when voice is not connected, another media mode is busy, or screenshare dependencies fail.
## Non-goals
- Replace the existing voice recorder pipeline.
- Disable message or voice monitoring during music playback.
- Build full production UI for screenshare controls in the first implementation.
- Add Discord integration tests that require a live account or server.
## Design
### Audio player ownership
`DiscordPlayer` will track which subsystem owns the active stream: `none`, `browser-bridge`, `music`, or `screen`. A caller may only start playback when the player has no owner or when the caller owns the current stream. This prevents the browser bridge from overwriting music or screen playback.
The browser bridge in `src/webserver.ts` will not start at server boot. It will be created lazily only when browser audio arrives and no media playback is active. When media playback starts, the bridge is stopped or left inactive so it cannot transmit captured audio back into Discord.
Music playback will claim the `music` owner before calling `playStream`. When music finishes or stops, ownership is released and browser audio may resume later if the browser sends new audio.
### Screenshare mode
The media queue endpoint will accept an optional `mode` field. If omitted, mode defaults to `music` to preserve existing API behavior. `mode: "screen"` starts a separate screenshare flow instead of audio-only music playback.
A new `ScreenShareController` will:
1. Verify a voice channel is connected.
2. Reject start if music or browser bridge owns playback, or if another screen stream is active.
3. Resolve a YouTube URL to a direct playable video URL through the existing yt-dlp utilities.
4. Use `@dank074/discord-video-stream` with `prepareStream(...)` and `playStream(..., { type: "go-live" })`.
5. Track active screen state and provide stop behavior.
Screenshare state will be exposed through media state as the active mode so the frontend can distinguish music from screen playback.
### Busy-state rules
- Music cannot start while screen is active.
- Screen cannot start while music is active.
- Browser bridge cannot start while music or screen is active.
- Stop stops the active media mode and releases ownership.
### Error handling
- `VOICE_NOT_CONNECTED`: media or screen requested before joining voice.
- `MEDIA_BUSY`: another active media mode owns playback.
- `SCREEN_STREAM_FAILED`: yt-dlp, stream preparation, or Go Live playback fails.
Errors should surface through existing Express error handling as JSON responses.
## Testing
- Unit test `DiscordPlayer` ownership rules: browser bridge cannot override music; music releases ownership on stop.
- Media controller tests: default mode remains music, screen mode is routed separately, and busy conflicts reject with `MEDIA_BUSY`.
- Route tests: `/api/media/queue` accepts optional `mode` and passes it to the controller.
- Screenshare controller tests mock yt-dlp and `@dank074/discord-video-stream`; no live Discord account is required.
## Rollout
Implement ownership first and verify existing music tests still pass. Then add mode parsing and the screenshare controller behind the same media route. UI changes can follow as a small enhancement after API behavior is stable.
@@ -1,87 +0,0 @@
# Session Full Recording Design
## Context
The recorder currently writes per-user OGG segments under `recordings/<userId>/`. Each segment has JSON metadata with user identity, bot flag, segment timing, and filename. The requested addition is a second recording view: one full-session OGG from the time the bot joins a voice channel until it leaves, while preserving the current per-user recording files.
Bot/self audio is excluded before segment creation, so session-level output should only include human participants.
## Goals
- Track one recording session from successful voice join until disconnect/leave.
- Preserve existing per-user OGG segment behavior.
- Create a background full-session OGG/Opus mix after the session ends.
- Store session metadata with duration, participants, segment references, output status, and full recording path.
- Keep muxing failures isolated from voice connection shutdown.
## Non-goals
- Real-time mixed full-session recording.
- Replacing per-user segment recording.
- Dashboard UI for session playback in this phase.
- Database-backed mux job retries in this phase.
## Output structure
A completed session writes:
```text
recordings/
sessions/
<recordingSessionId>/
full.ogg
session.json
```
`recordingSessionId` is based on guild ID, channel ID, and session start time: `<guildId>-<channelId>-<sessionStartTime>`.
`session.json` contains:
- `sessionId`
- `guildId`
- `channelId`
- `channelName`
- `startTime`
- `endTime`
- `durationMs`
- `status`: `completed`, `failed`, or `empty`
- `outputFile`: relative path to `full.ogg` when present
- `participants`: non-bot users observed in the session
- `segments`: per-user segment metadata references with absolute timing
- `error`: failure message when muxing fails
Per-user segment JSON also records the shared `recordingSessionId` so full-session muxing can identify which files belong to the same join/leave session.
## Lifecycle
1. `startRecording()` creates a session object after the voice connection reaches ready state.
2. Each non-bot speaking user still gets the existing per-user `SegmentManager` flow.
3. Each finished segment is registered with the active session using its metadata path, OGG path, user ID, start time, and end time.
4. `stopRecording(guildId)` or connection destruction finalizes the active session with `endTime`.
5. Finalization starts muxing in the background and does not block disconnect.
6. Muxing writes `session.json` with `empty`, `completed`, or `failed` status.
## Muxing design
The post-processor reads all registered segment metadata for the session. It builds an ffmpeg `filter_complex` that delays each input by `segment.startTime - session.startTime` milliseconds, mixes all delayed inputs with `amix`, and encodes the result to OGG/Opus.
For a session with no human segments, muxing skips ffmpeg and writes `session.json` with `status: "empty"` and the full session duration.
For successful muxing, it writes `full.ogg` and `session.json` with `status: "completed"`.
For failed muxing, it writes `session.json` with `status: "failed"` and the error message.
## Error handling
- Failure to write `session.json` is logged and does not crash shutdown.
- ffmpeg failure is captured in metadata as `status: "failed"`.
- Missing or empty segment files are skipped from the mix and recorded as skipped references if needed.
- Background mux errors never reject `stopRecording()`.
## Testing
- Unit test session metadata creation from join to stop.
- Unit test bot/self users do not register participants or segments.
- Unit test mux filter generation with timeline offsets.
- Unit test empty sessions write `status: "empty"` without calling ffmpeg.
- Unit test stop triggers background finalization without awaiting ffmpeg.
@@ -1,85 +0,0 @@
# Internal Streamer Replacement Design
## Summary
Replace the external `@dank074/discord-video-stream` dependency with an internal streaming module that uses `discord.js-selfbot-v13` private APIs to deliver the same screen share behavior (video + audio) with identical UI/API surface.
## Goals
- Maintain feature parity for screen share (video + audio, 720p @ 30fps, bitrate 2500/4000, H264, audio on).
- Keep existing UI and API contracts unchanged (`/api/media/queue` with `mode: "screen"`).
- Remove `@dank074/discord-video-stream` from dependencies and delete `vendor/Discord-video-stream`.
- Ensure clean lifecycle handling (start/stop, cleanup, error reporting).
## Non-Goals
- Rewriting WebRTC/RTP stack from scratch.
- Changing media queue behavior or UI layout.
- Adding new screen share modes or settings.
## Architecture Overview
Introduce a new internal module under `src/streaming/` that encapsulates:
- Voice/session management using private `discord.js-selfbot-v13` APIs.
- FFmpeg preparation for H264 + Opus (AnnexB video + Opus audio).
- Stream playback into the internal dispatcher.
`screenShareController` will depend on this module instead of `@dank074/discord-video-stream`.
## Components
### 1) Streaming Session Module (`src/streaming/`)
Proposed exports:
- `createStreamSession(client)`
- Joins or reuses voice connection for video streaming.
- Exposes a `session` object with `startVideo()`, `stopVideo()`, and `sendStream(stream)` hooks.
- `prepareFfmpegStream(source, opts)`
- Spawns ffmpeg with the same parameters used today.
- Returns `{ command, output }` (output is a Readable stream).
- `playPreparedStream(output, session)`
- Pipes the prepared stream into the internal dispatcher.
- Returns a promise that resolves when playback completes.
### 2) Screen Share Controller (`src/media/screenShareController.ts`)
- Replace Streamer/prepareStream/playStream with internal module usage.
- Keep the public API identical (`start(source)` returning `ScreenSharePlayback`).
### 3) Web Server Wiring (`src/webserver.ts`)
- Remove `Streamer` instantiation and dependencies.
- Pass only `getVoiceStatus` and new streaming module dependencies into `createScreenShareController`.
## Data Flow
1. User queues screen share via `/api/media/queue` with `mode: "screen"`.
2. `MediaController` calls `screenShareController.start(source)`.
3. `screenShareController` resolves URL, calls `prepareFfmpegStream`.
4. `createStreamSession` ensures voice connection and dispatcher ready.
5. `playPreparedStream` sends output to Discord.
6. On completion or stop, cleanup runs and state updates propagate.
## Error Handling
- Voice not connected: throw `VOICE_NOT_CONNECTED`.
- FFmpeg spawn/exit failure: throw `SCREEN_STREAM_FAILED`.
- Dispatcher error: stop stream, cleanup, log error, set state idle.
## Lifecycle Rules
- `start()` always stops any active stream first.
- `stop()` kills ffmpeg, stops dispatcher, and resets internal state.
- Completion resolves `done` promise and triggers cleanup.
## Testing Strategy
- Unit tests for `screenShareController`:
- Calls to `prepareFfmpegStream` and `playPreparedStream` on `start()`.
- Ensures `stop()` kills ffmpeg and ends session.
- Unit tests for `streaming` module:
- Session initialization and cleanup logic with mocked private APIs.
## Migration Steps
1. Implement `src/streaming/` module.
2. Update `screenShareController` to use internal module.
3. Remove `@dank074/discord-video-stream` imports and wiring.
4. Delete `vendor/Discord-video-stream` directory.
5. Update `package.json` dependencies.
6. Update tests.
## Risks
- Private `discord.js-selfbot-v13` APIs may change.
- Harder debugging if internal dispatcher behavior differs.
## Rollback Plan
- Revert to previous commit that restores `@dank074/discord-video-stream` and the vendor directory.
@@ -1,56 +0,0 @@
# Design Spec: Robust Moderation & Test Improvements
**Date**: 2026-05-18
**Topic**: Robust LLM Moderation Parsing, Capping Multimodal Attachments, and Fixing Dev/Streaming Tests
---
## 1. Goal & Context
The project contains an LLM-based content moderation system that analyzes Discord messages and their image attachments. Real-world utilization revealed several issues:
1. **Multimodal API Limits**: High numbers of image attachments in the target or surrounding context messages exceed API limits (e.g. Nemotron/Omni models cap at 8 images), triggering an HTTP 400 error.
2. **LLM Output Variance**: LLM responses containing reasoning processes, conversational preambles, or markdown wrappers fail to parse under the current naive brace-matching algorithm, yielding `No JSON object found` or `Response missing 'results' array`.
3. **Snowflake Precision Loss**: Snowflake IDs returned by the LLM sometimes suffer from floating-point rounding or formatting issues, preventing them from matching the original string-based target IDs.
4. **Dev/Streaming Test Failures**: Failing tests in `ytdlp.test.ts` and `playTranscode.test.ts` due to mismatched parameters and type assertions.
---
## 2. Architecture & Detailed Design
### A. Multimodal Attachment Filtering & Prioritization
In `src/moderation/llmModerationClient.ts`:
* Extract all image attachments.
* Sort and prioritize attachments:
* Targets first: Attachments belonging to messages in the active `targets` list.
* Context second: Attachments belonging to context messages, sorted by `created_at` descending (most recent first).
* Slice the resulting array to a maximum of **8 elements** to ensure we never hit model limits.
* If the list is empty, proceed with the existing transparent 1x1 dummy PNG fallback.
### B. Resilient JSON Extraction
Implement `extractJson` inside `src/moderation/llmModerationClient.ts`:
1. **Markdown Blocks**: Scan for code blocks using `/```(?:json)?\s*([\s\S]*?)\s*```/g`. Try to parse the first match yielding an object.
2. **Exhaustive Span Search**: If markdown parsing fails, locate the indices of all `{` and `}` characters in the string. Try all matching pairs, starting from the largest span to the smallest.
3. **Error Reporting**: If no candidate substring parses as an object, throw `No JSON object found in response`.
### C. Message ID Fuzzy Mapping
* Map `message_id` back to target IDs by stringifying and checking exact match.
* If not matched and the ID ends with `"00"` or contains `"e+"` (indicating exponential format or floating point precision loss), search `targetIds` for a prefix match (first 10 characters) and restore the original ID.
### D. Streaming & Dev Test Fixes
* **`tests/media/ytdlp.test.ts`**: Update the assertion to expect `--format best[protocol^=http]/best` to match the actual production code.
* **`tests/streaming/playTranscode.test.ts`**: Safely check if the input `readable` is an object and has the `.on` function before calling `readable.on("data", ...)`.
---
## 3. Test Plan & Expanded Coverage
We will implement dedicated unit tests in `tests/moderation/llmModerationClient.test.ts`:
1. **Image Capping & Prioritization**: Ensure image attachments are sorted correctly and capped at 8.
2. **Complex Conversational Content**: Verify extraction from messages wrapped in markdown, with leading/trailing text, and multiple code blocks.
3. **Reasoning Blocks**: Verify extraction when reasoning blocks contain separate `{` and `}` symbols.
4. **Precision Loss Scenarios**: Verify automatic correction of floating-point string representations of Snowflake IDs.
---
## 4. Success Criteria
* All tests pass successfully (`pnpm run test` exits with `0`).
* System remains highly resilient to formatting variance in LLM responses.
* No 400 Bad Request errors occur due to exceeding the maximum image attachment limit.
@@ -1,223 +0,0 @@
# Aggressive Codebase Restructure Design
## Goal
Restructure the codebase aggressively enough to make ownership clear, while preserving runtime behavior and public contracts. The first target is the oversized webserver/bootstrap area, then dependency cleanup after the new boundaries compile and pass tests.
## Scope
In scope:
- Split `src/webserver.ts` responsibilities into focused modules.
- Move bootstrap and shutdown lifecycle out of `src/index.ts`.
- Keep existing REST endpoints, WebSocket payloads, dashboard behavior, and Discord audio behavior unchanged.
- Add small tests for extracted pure logic.
- Audit dependencies after structure stabilizes, then remove, move, or replace only dependencies proven unused or misplaced.
Out of scope:
- Changing Discord moderation, voice recording, media playback, or dashboard features.
- Replacing the selfbot library.
- Reworking database schema or migrations.
- Removing the temporary `globalThis` broadcast compatibility layer in this pass.
## Target Structure
```text
src/
app/
bootstrap.ts
shutdown.ts
http/
app.ts
server.ts
health.ts
ws/
server.ts
voiceAudioBridge.ts
broadcastGlobals.ts
state/
uiState.ts
mediaSettings.ts
audio/
pcm.ts
routes/
...existing
```
### `src/app/bootstrap.ts`
Owns application startup:
1. Initialize database.
2. Create Discord client and `VoiceController`.
3. Register Discord debug/error handlers.
4. On ready, register moderation capture, start AI worker, start backlog sync, and start HTTP/WebSocket server.
5. Start Discord login.
### `src/app/shutdown.ts`
Owns graceful shutdown:
- Close database.
- Disconnect voice controller.
- Pause player.
- Destroy Discord client.
- Exit with correct status.
The shutdown module receives dependencies instead of importing mutable singletons where practical.
### `src/http/app.ts`
Creates and configures Express:
- Helmet with existing CSP setting.
- API no-store middleware.
- HTTP error logging.
- JSON body parser.
- Static dashboard serving.
- Route mounting.
- Express error handler.
### `src/http/health.ts`
Contains health, metrics, and auth routes currently inline in `webserver.ts`.
### `src/http/server.ts`
Creates HTTP server, WebSocket server, broadcaster, media controller, stream controller, and route dependencies. Starts listening on `0.0.0.0` using existing port behavior.
### `src/ws/server.ts`
Owns WebSocket lifecycle:
- Accept connections.
- Register clients with broadcaster.
- Send initial `user_state`, `ui_state`, and `media_state` messages.
- Route binary PCM messages to voice audio bridge.
- Remove clients on close/error.
### `src/ws/voiceAudioBridge.ts`
Owns browser PCM to Discord audio:
- Upsample 24kHz mono PCM to 48kHz stereo PCM.
- Keep existing 20ms pull loop.
- Preserve silence tail, max buffer, owner checks, pause/unpause behavior, and logging.
- Use extracted pure helpers from `src/audio/pcm.ts`.
### `src/ws/broadcastGlobals.ts`
Contains the temporary compatibility layer for existing recorder/moderation code:
- `moderationBroadcaster`
- `broadcastPcmToWeb`
- `broadcastVideoToWeb`
- `updateActiveUser`
- `ADMIN_PASSWORD`
This isolates `globalThis` usage so later work can replace it with explicit dependency injection.
### `src/state/uiState.ts`
Owns persisted UI state:
- Defaults.
- `normalizeSharedUIState`.
- Initialize, get, and patch helpers.
### `src/state/mediaSettings.ts`
Owns persisted media settings:
- Defaults.
- Initialize helper.
- Update helper for music volume.
### `src/audio/pcm.ts`
Owns pure PCM utilities:
- `upsample24kMonoTo48kStereo`.
- `rmsDb`.
## Data Flow
```text
index.ts
-> bootstrap app
-> initializeDatabase()
-> create Discord client + VoiceController
-> on ready:
-> register moderation capture
-> start AI worker + backlog sync
-> startHttpServer()
startHttpServer()
-> create Express app
-> create HTTP server
-> create WebSocket server
-> create broadcaster
-> create Streamer + ScreenShareController + MediaController
-> mount API routes
-> expose temporary global hooks
```
## Behavior Preservation
Do not change:
- REST endpoint paths or JSON shapes.
- WebSocket outbound JSON message types.
- WebSocket binary voice/video packet format.
- Browser PCM assumptions: 24kHz mono signed 16-bit little-endian.
- Discord outbound audio assumptions: 48kHz stereo Opus frames.
- Static dashboard fallback behavior.
- Existing logging messages unless moving them requires minor context changes.
## Dependency Cleanup
Dependency cleanup happens after structural refactor passes validation.
Process:
1. Build an import map from source, tests, scripts, frontend, and config files.
2. Identify unused dependencies and devDependencies.
3. Move packages used only by tooling/tests/frontend into correct dependency class if currently misplaced.
4. Remove package entries only when no import, config usage, script usage, or runtime side-effect import exists.
5. Prefer not adding libraries unless they replace custom fragile code or fill a concrete missing capability.
Potential new tool dependency: `depcheck` may be used as a one-off via `pnpm dlx depcheck`, not necessarily added to `package.json`.
## Testing Plan
Add or update tests for extracted pure logic:
- UI state normalization keeps legacy `selectedGuild` behavior.
- PCM upsample doubles sample rate and duplicates mono into stereo channels.
- RMS dB handles normal PCM input consistently.
Run validation after each major step:
```bash
pnpm run typecheck
pnpm run lint
pnpm run test
pnpm run build
```
## Risks
- Aggressive file movement can break imports. Mitigation: move one responsibility at a time and run typecheck frequently.
- `globalThis` compatibility can hide coupling. Mitigation: isolate it in `broadcastGlobals.ts` and avoid expanding it.
- Dependency cleanup can remove runtime side-effect packages. Mitigation: treat side-effect imports in `src/index.ts` as used.
- Frontend and backend share one package manifest. Mitigation: audit source areas separately before moving dependencies.
## Acceptance Criteria
- `src/webserver.ts` is removed or reduced to a thin compatibility facade.
- `src/index.ts` delegates startup to `src/app/bootstrap.ts`.
- HTTP, WebSocket, UI state, media settings, audio bridge, and broadcast globals have separate modules.
- Existing tests pass.
- Typecheck, lint, and build pass.
- Dependency changes are justified by import/config/script evidence.
@@ -1,91 +0,0 @@
# Deprecated Dependency Removal Design
## Goal
Remove deprecated packages from the pnpm lockfile where practical. Prefer maintained replacements or upgrades. If no maintained replacement exists, vendor upstream code as a submodule/workspace and patch dependency metadata there.
## Scope
Current deprecated sources:
- `drizzle-kit` pulls `@esbuild-kit/esm-loader` and `@esbuild-kit/core-utils`.
- `discord.js-selfbot-v13` pulls `otplib@12` plugins.
- `@discordjs/opus` pulls `@discordjs/node-pre-gyp`, which pulls `npmlog`, `are-we-there-yet`, `gauge`, `rimraf@3`, `glob@7`, and `inflight`.
- `@lng2004/node-datachannel` and `better-sqlite3` pull `prebuild-install`.
Existing workspace packages:
- `vendor/discord.js-selfbot-v13`
- `vendor/discord-video-stream`
## Approach
1. Upgrade direct dependencies first and re-check `pnpm why` plus npm deprecation metadata.
2. Patch vendored workspace dependencies when project already owns package source.
3. Replace direct packages only when runtime compatibility is clear.
4. Add submodules only for packages that cannot be replaced or upgraded without keeping deprecated transitive packages.
## Package Plan
### `drizzle-kit`
Try latest compatible `drizzle-kit`. If latest still depends on `@esbuild-kit/*`, keep current version unless project commands fail, because vendoring `drizzle-kit` only to remove dev-only install warnings has high maintenance cost.
### `discord.js-selfbot-v13`
Patch `vendor/discord.js-selfbot-v13` dependency graph to remove `otplib@12` if code is compatible with `otplib@13`. Verify by installing and running typecheck/tests. Keep peer/package name unchanged.
### `@discordjs/opus`
Find maintained Opus alternative compatible with current recorder and `@discordjs/voice`. Prefer removing direct `@discordjs/opus` only if code and tests still pass. If native Opus remains needed and every maintained option drags deprecated install tooling, vendor the smallest dependency owner.
### `prebuild-install` sources
Do not patch native package install chains blindly. For `better-sqlite3`, keep upstream unless latest removes `prebuild-install`. For `@lng2004/node-datachannel`, try latest first through `discord-video-stream`; vendor only if strict lockfile cleanup remains blocked and build still works.
### `discord-video-stream`
Keep as workspace submodule. Patch devDependency `discord.js-selfbot-v13` to use workspace reference so installs do not fetch deprecated registry selfbot.
## Verification
After each dependency change:
1. Run `pnpm install`.
2. Run `pnpm why` for known deprecated package names.
3. Check npm deprecation metadata for remaining lockfile packages.
4. Run `pnpm run typecheck`.
5. Run `pnpm run test`.
## Success Criteria
- Root `package.json` uses workspace paths for vendored packages.
- `pnpm-lock.yaml` has no deprecated packages where maintained replacements exist.
- Any remaining deprecated packages are documented as no-maintained-replacement and owned by a vendored submodule or unavoidable native upstream.
- Typecheck and tests pass.
## Final Audit Result
Deprecated packages removed from active dependency graph:
- `@otplib/plugin-crypto`, `@otplib/plugin-thirty-two`, `@otplib/preset-default` — removed by patching `vendor/discord.js-selfbot-v13` to `otplib@13`.
- `@discordjs/opus` direct dependency — removed from root dependencies.
- `@discordjs/node-pre-gyp`, `npmlog`, `are-we-there-yet`, `gauge`, `rimraf@3`, `glob@7`, `inflight` — removed by eliminating `@discordjs/opus` auto-installed peer path.
Remaining unavoidable deprecated packages:
- `@esbuild-kit/core-utils@3.3.2` via `drizzle-kit@0.31.10`.
- `@esbuild-kit/esm-loader@2.6.5` via `drizzle-kit@0.31.10`.
- `prebuild-install@7.1.3` via `better-sqlite3@12.10.0` and `@lng2004/node-datachannel@0.32.0-20260202`.
Reason these remain:
- `drizzle-kit@0.31.10` is latest stable and still depends on `@esbuild-kit/*`.
- `better-sqlite3@12.10.0` is latest stable and still uses `prebuild-install` for native binary install.
- `@lng2004/node-datachannel@0.32.0-20260202` is latest available and still uses `prebuild-install` for native binary install.
The upstream repositories are vendored as submodules for future patching if strict zero-deprecated lockfile becomes worth maintaining as forks:
- `vendor/drizzle-orm`
- `vendor/better-sqlite3`
- `vendor/node-datachannel`
@@ -1,115 +0,0 @@
# Winston Logging Refactor Design
## Goal
Refactor project logging from Pino to Winston and clean up logging-related dependencies without changing application behavior.
## Scope
- Replace `pino` and `pino-pretty` with `winston`.
- Remove `pino-http` if no code still uses it.
- Keep logging access centralized in `src/logger.ts`.
- Preserve current exported logger API shape where practical: `logger` and `createChildLogger(context)`.
- Normalize log output and levels across the codebase.
- Avoid unrelated feature work or broad refactors.
## Logging Architecture
`src/logger.ts` remains the only logging entry point. It will create one Winston logger with npm levels:
- `error`
- `warn`
- `info`
- `http`
- `verbose`
- `debug`
- `silly`
`LOG_LEVEL` validation will be updated to accept these Winston standard levels. Default behavior stays environment-aware: development can log more detail, production stays concise.
## Outputs
Winston will write to:
1. Console
- Pretty, colorized, timestamped output.
- Includes logger context, message, and metadata.
2. `logs/app.log`
- JSON format.
- Includes all logs at configured level and above.
3. `logs/error.log`
- JSON format.
- Includes error-level logs only.
The logger should create the `logs/` directory at runtime if needed. `logs/` should be ignored by git.
## Metadata and Error Handling
Existing log calls mostly remain valid. `src/logger.ts` will format metadata centrally so individual call sites do not need custom serialization.
Handled metadata shapes:
- `{ error: err }`
- `{ err }`
- `{ reason }`
- extra plain objects used by existing log calls
Errors should serialize with message, stack, name, code, statusCode, and any relevant enumerable fields. Non-error metadata should pass through without lossy conversion.
## Code Changes
Expected files:
- `package.json` and lockfile: add `winston`, remove Pino packages that become unused.
- `src/logger.ts`: rewrite from Pino to Winston.
- `src/config.ts`: expand `LOG_LEVEL` schema.
- `.gitignore`: ignore `logs/` if missing.
- Logger call sites: update only if TypeScript or Winston format compatibility requires it.
Known logger consumers include:
- `src/index.ts`
- `src/webserver.ts`
- `src/middleware.ts`
- `src/voiceController.ts`
- `src/media/mediaController.ts`
- `src/media/screenShareController.ts`
- `src/moderation/broadcaster.ts`
- `src/moderation/messageCapture.ts`
- `src/streaming/transcoder.ts`
Additional consumers should be found by grep during implementation.
## Dependency Cleanup
Remove packages only after confirming no imports remain:
- `pino`
- `pino-pretty`
- `pino-http`
Do not remove unrelated dependencies in this pass.
## Testing and Verification
Run:
- `pnpm install` or equivalent lockfile update after dependency changes.
- `pnpm run typecheck`
- `pnpm run test`
- `pnpm run lint`
Also verify log behavior with a short runtime command or startup check:
- console output is readable and includes context.
- `logs/app.log` is created.
- `logs/error.log` is created when an error log occurs.
## Success Criteria
- No Pino imports remain.
- Winston is the only logging backend.
- Existing application logging calls compile.
- Log levels are consistent and configurable via `LOG_LEVEL`.
- Console and file logging both work.
- Tests, typecheck, and lint pass.
@@ -1,619 +0,0 @@
# Bete Frontend Redesign — IMPHNEN Design System Deep Integration
**Date:** 2026-07-02
**Status:** Design Spec (draft)
**Project:** Bete — Guild Moderation Watcher for IMPHNEN Discord
**Approach:** "Deep Foundation" (Layer 0 → 4)
---
## Overview
Bete is a real-time Discord moderation dashboard serving the IMPHNEN community. The frontend (React 19 + Vite 8 + Tailwind 4) already has the IMPHNEN design tokens partially applied, but suffers from:
1. **Fragmented color system** — many components bypass CSS variables with hardcoded Tailwind utility colors (emerald, amber, orange, red — documented in `DESIGN_TOKENS.md` §13)
2. **No dark mode** — the design system only defines light tokens
3. **Inconsistent brand expression** — Sidebar collapses brand assets, navigation feels detached from community identity
4. **Mobile experience is minimal** — only a basic `MobileTabBar`
5. **Technical debt** in animation keyframes, z-index scale, component variant consistency
This design spec outlines a systematic redesign across 5 layers, executed top-to-bottom (foundation → components → layout → features → polish), with **light + dark mode support** baked in from the start.
---
## Layer 0: Design Tokens & Theme System
### Current State
- CSS custom properties defined in `styles.css` under `:root` — light only
- Tailwind config duplicates token definitions in JS — fragmentasi source of truth
- `DESIGN_TOKENS.md` documents 13 known issues, mostly hardcoded hex colors bypassing the token system
### Proposed System
#### Theme Engine
CSS-first approach. Tailwind 4 `@theme` directive references CSS custom properties. Dark mode driven by `prefers-color-scheme` and manual `[data-theme="dark"]` attribute toggle.
```css
/* styles.css */
:root {
/* Light tokens */
--surface: #ffffff;
--on-surface: #1a1a1a;
--primary: #23a1eb;
--primary-soft: #e1f0fd;
--border: #e0e0e0;
/* … */
}
[data-theme="dark"] {
--surface: #1c1c1f;
--on-surface: #f0f0f2;
--primary: #54a2ff;
--primary-soft: #18263a;
--border: #343438;
/* … */
}
@theme {
--color-primary: var(--primary);
--color-primary-soft: var(--primary-soft);
/* … */
}
```
#### Dark Mode Palette
| Token | Light | Dark |
|-------|-------|------|
| `--surface` | `#ffffff` | `#1c1c1f` |
| `--surface-dim` | `#f5f5f5` | `#141417` |
| `--surface-container` | `#e8e8e8` | `#26262a` |
| `--surface-bright` | `#ffffff` | `#2c2c30` |
| `--on-surface` | `#1a1a1a` | `#f0f0f2` |
| `--on-surface-variant` | `#666666` | `#a0a0a6` |
| `--inverse-surface` | `#1a1a1a` | `#f0f0f2` |
| `--inverse-on-surface` | `#f5f5f5` | `#1a1a1a` |
| `--primary` | `#23a1eb` | `#54a2ff` |
| `--primary-soft` | `#e1f0fd` | `#18263a` |
| `--primary-hover` | `#1a8fd9` | `#3d8ee8` |
| `--secondary` | `#1877f2` | `#4a8ef5` |
| `--secondary-soft` | `#e7f1ff` | `#1a274a` |
| `--tertiary` | `#5865f2` | `#7984f5` |
| `--tertiary-soft` | `#eef0ff` | `#20266a` |
| `--border` | `#e0e0e0` | `#343438` |
| `--border-hover` | `#cccccc` | `#48484d` |
| `--outline` | `#999999` | `#6a6a70` |
| `--outline-variant` | `#cccccc` | `#404044` |
| `--success` | `#22c55e` | `#34d399` |
| `--success-soft` | `#dcfce7` | `#13261a` |
| `--warning` | `#f59e0b` | `#fbbf24` |
| `--warning-soft` | `#fef3c7` | `#261a10` |
| `--destructive` | `#e4405f` | `#f87171` |
| `--destructive-soft` | `#ffebee` | `#2a1418` |
| `--info` | `#3b82f6` | `#60a5fa` |
| `--info-soft` | `#dbeafe` | `#141e38` |
#### Theme Switching
- **Default**: follow `prefers-color-scheme`
- **Manual toggle**: `[data-theme="dark"]` attribute on `<html>`, persisted to `localStorage`
- **Transition**: CSS `transition: background-color 200ms, color 150ms` on `body` and key containers
- All components respond without re-render since they reference CSS variables
#### Migration Strategy (Hardcoded Hex → CSS Vars)
All components documented in `DESIGN_TOKENS.md` §13 will be migrated:
**Before** (example from `MessageCard.tsx`):
```tsx
className="bg-red-100 text-red-700 border-red-200"
```
**After**:
```tsx
className="bg-destructive-soft text-destructive border-destructive/20"
```
Where `--destructive-soft`, `--destructive`, `--destructive/20` are CSS variablebacked.
#### Z-Index Registry
Standardize z-index stack to avoid collisions:
| Value | Component |
|-------|-----------|
| `10` | Sticky Header |
| `20` | Sidebar |
| `30` | Tab Strip (sticky if scrolled) |
| `40` | Toast Container |
| `50` | Mobile Bottom Nav |
| `60` | Mascot Chatbot |
| `70` | Modal / Dialog |
| `100` | Overlay backdrop |
---
## Layer 1: Shared UI Components
### Guiding Principle
Every component matches exactly **one** design token reference per visual property. No component uses raw Tailwind utility colors like `emerald-*`, `amber-*`, `red-*` — only CSS variables.
### Component Audit & Changes
#### Button (`shared/ui/button.tsx`)
**Status:** Mostly good — uses CSS vars already ✅
**Changes:**
- Add `variant: "tertiary"` (#5865f2 / #7984f5 dark) for Discord-specific actions
- Add `size: "icon-sm"` (h-8 w-8, h-4 w-4 icon) for compact icon buttons
- Ensure `active:scale-[0.97]` is consistent across all variants
#### Badge (`shared/ui/badge.tsx`)
**Status:** Needs fix — `success` uses hardcoded `emerald-100 text-emerald-700`
**Changes:**
- Map `success``bg-success-soft text-success`
- Map `warning``bg-warning-soft text-warning`
- Add `variant: "tertiary"` for Discord brand accent
- All variants reference CSS variables
#### Card (`shared/ui/card.tsx`)
**Status:** Good — `shadow-sm hover:shadow-md transition-shadow`
**Changes:**
- Add `variant: "elevated"` for modals/important containers (shadow-md default, shadow-lg hover)
- Add `variant: "bordered"` for inline containers (border only, no shadow)
#### Input (`shared/ui/input.tsx`)
**Status:** Good — proper focus ring, border ✅
**Changes:**
- Add `variant: "soft"` for search/filter inputs (muted background, no border on idle)
- Add `disabled` styling that's distinct (not just opacity-50)
#### Select (`shared/ui/select.tsx`)
**Status:** Same as Input — needs soft variant ✅
**Changes:**
- Same `variant: "soft"` for filters
- Chevron icon should use primary color on focus
#### Toast (`shared/ui/toast.tsx`)
**Status:** Needs rewrite — `emerald-500`/`amber-500` hardcoded ❌
**Changes:**
- Replace `border-l-emerald-500 text-emerald-500``border-l-success text-success`
- Replace `border-l-amber-500 text-amber-500``border-l-warning text-warning`
- Change z-index from `z-50` to `z-40` to avoid colliding with MobileTabBar
- Stack animation: slide-in from right, fade-out
#### Skeleton (`shared/ui/skeleton.tsx`)
**Status:** `animate-shimmer` duration mismatch between CSS (1.5s) and Tailwind config (2s) ❌
**Changes:**
- Pick **CSS as canonical** (1.5s ease-in-out)
- Remove duplicate from Tailwind config
- Add `variant` prop: `rounded`, `circular`, `rectangular`
- Add `width`/`height` props with sensible defaults
#### Tabs (`shared/ui/tabs.tsx`)
**Status:** Good — Radix-based, uses CSS vars ✅
**Changes:**
- Add `variant: "pills"` for filter-style tabs (used in Messages filter bar)
- Add `variant: "underline"` for navigation tabs (used instead of sidebar tab switching)
- TabsContent should have `mt-6` spacing consistent
#### ScrollArea (`shared/ui/scroll-area.tsx`)
**Status:** Good ✅
**Changes:**
- Style scrollbar thumb with design system `--border` and `--primary` on hover
### New Components
#### Switch (`shared/ui/switch.tsx`)
- Radix `Switch` primitive
- Colors: `--border` (off), `--primary` (on), thumb white
- Dark mode aware via CSS vars
- For settings toggles (future: retention policies, auto-delete config)
#### Dialog (`shared/ui/dialog.tsx`)
- Radix `Dialog` primitive
- Overlay: `bg-black/40 backdrop-blur-sm`
- Content: Card variant `elevated`, max-w-md, centered
- Enter: scaleIn + fadeIn animations
- For confirmations, warnings, and future settings panels
#### DropdownMenu (`shared/ui/dropdown-menu.tsx`)
- Radix `DropdownMenu` primitive
- Trigger: ghost button
- Content: card surface with shadow-lg, rounded-lg
- Items: list-item pattern with hover state
- For overflow actions (message actions, bulk operations)
#### EmptyState (`shared/ui/empty-state.tsx`)
Standard empty/error/loading state component:
- Props: `icon` (Lucide), `title` (string), `description` (string), `action` ({label, onClick} | ReactNode)
- Visual: centered flexbox, mascot-style illustration or icon, muted text
- Animation: fade-in-up on mount
- All feature panels use this — no more ad-hoc empty states
#### IconButton (`shared/ui/icon-button.tsx`)
- Sizes: `sm` (h-8 w-8), `md` (h-10 w-10), `lg` (h-12 w-12)
- Variants: `ghost`, `outline`, `primary`, `destructive`, `tertiary`
- Tooltip on hover (custom or Radix Tooltip)
- For action buttons in MessageCard (reanalyze, moderate, etc.)
---
## Layer 2: Layout & Navigation
### Guiding Principle
The layout is the "frame" for IMPHNEN's brand identity. Every structural decision reinforces the community-centered moderation tool narrative.
### Desktop Layout
```
┌──────────────────────────────────────────────────────────────────┐
│ ┌─────────┐ ┌──────────────────────────────────────────────┐ │
│ │ │ │ [Logo] IMPHNEN ● ● ● │ │
│ │ SIDEBAR │ │ Guild Watcher 🟢 Online 🟡 Idle│ │
│ │ w-64 │ ├──────────────────────────────────────────────┤ │
│ │ fixed │ │ │ │
│ │ │ │ ┌── TAB STRIP ──────────────────────────┐ │ │
│ │ ● Logo │ │ │ [≡] Pesan Voice Dashboard │ │ │
│ │ "IMPH- │ │ └──────────────────────────────────────┘ │ │
│ │ NEN" │ │ │ │
│ │ Guild │ │ ┌── MAIN CONTENT (max-w-1280) ─────────┐ │ │
│ │ Watcher │ │ │ │ │ │
│ │ │ │ │ [Page title] │ │ │
│ │ ─────── │ │ │ [Subtitle] │ │ │
│ │ │ │ │ │ │ │
│ │ [💬] │ │ │ [Content — cards, lists, grids] │ │ │
│ │ Pesan │ │ │ │ │ │
│ │ [📡] │ │ │ │ │ │
│ │ Voice │ │ │ │ │ │
│ │ [📊] │ │ │ │ │ │
│ │ Guild │ │ │ │ │ │
│ │ │ │ └──────────────────────────────────────────┘ │
│ │ ─────── │ │ │ │
│ │ 🐱 │ │ │ │
│ │ Mascot │ │ │ │
│ └─────────┘ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```
#### Sidebar
- **Desktop**: `w-64`, **expanded by default** with mascot visible
- Collapsible via hamburger toggle to `w-16` (icon-only)
- **Brand section**: Logo + gradient "IMPHNEN" text + "Guild Watcher" subtitle + live dot
- **Nav items**: Icon + label, active state with `--primary-soft` background
- **Mascot section**: Small mascot image (clickable → chat), active status, at bottom
- **Border-right**: `border-[--border]`
- **Sticky**: Full height, fixed on scroll
#### Header (Brand Bar)
- **Sticky top-0**, `z-10`
- **Left**: Logo + brand name (40% width)
- **Right**: Status badges row (WS indicator, Voice indicator, Theme toggle, Notification bell)
- **Background**: `bg-surface/70 backdrop-blur-md`
- **Border-bottom**: `border-[--border]`
- **Height**: `h-14` (56px) — more compact than current (h-16)
#### Tab Strip
- **Horizontal** row below header, between brand bar and content
- **Tabs**: `[Messages] [Voice & Media] [Dashboard Guild]`
- **Active indicator**: underline bar (selected) or filled pill
- **Sticky** with `z-20` when content scrolls
- On mobile: horizontal scroll with snap points
#### Content Area
- **Max-width**: `1280px`
- **Padding**: `p-4 md:p-6 lg:p-8`
- **Page title + subtitle** rendered inside content (not header), consistent across all 3 tabs
- **Grid gap**: `gap-6` between sections, `gap-4` between cards in a grid
### Mobile Layout
```
┌─────────────────────────┐
│ [←] IMPHNEN ☰ 🌙 │ ← Compact header (brand + toggle)
├─────────────────────────┤
│ Tab Strip (horizontal │
│ scroll, snap points) │
├─────────────────────────┤
│ │
│ Content (single col, │
│ stacked vertically) │
│ │
│ │
├─────────────────────────┤
│ 💬 📡 📊 🐱 │ ← Bottom Nav (safe area)
└─────────────────────────┘
```
- **Header**: Compact — brand + collapse/expand + theme toggle
- **Tab Strip**: Horizontal scroll, no Sidebar
- **Bottom Nav**: 4 tabs (Messages, Voice, Dashboard, Mascot) with active state indicator
- **Safe area**: `pb-4 md:pb-0` for notched devices
- **Particle background**: Skipped entirely on mobile
### Particle Background
**Status:** Three.js canvas, always rendered ✅ but heavy
**Changes:**
- Only render when `prefers-reduced-motion: no-preference`
- **Mobile**: Skip entirely (conditional render via `useMediaQuery`)
- **Desktop**: Reduce particle count from current (default) to ~30 orbs
- Use CSS `opacity` transition for mount/unmount
- Color references `--primary` and `--tertiary` from CSS vars (currently hardcoded hex)
---
## Layer 3: Feature Components
### Guiding Principle
Every feature panel uses the same design language — same spacing, same typography, same color semantics, same interaction patterns. A user switching between Messages and Dashboard should feel like the same app.
### MessageCard (`features/messages/components/MessageCard.tsx`)
**Visual structure:**
```
┌─────────────────────────────────────────────────────────┐
│ │
│ [Avatar] username [Badge] timestamp [actions] │
│ │
│ Message content (word-wrap, max-height truncated) │
│ │
│ [flag badge 1] [flag badge 2] [flag badge 3] │
│ │
│ ────────────────────────────────────────────── │
│ AI: Analysis summary (1-2 lines, collapsible) │
│ │
└─────────────────────────────────────────────────────────┘
```
**Changes:**
- Replace all hardcoded status colors with `Badge` component variants
- `ai_status="clean"``<Badge variant="success">`
- `ai_status="flagged"``<Badge variant="destructive">`
- `ai_status="error"``<Badge variant="warning">`
- `ai_status="pending"``<Badge variant="outline">`
- AI analysis section collapsible (default collapsed, show first 80 chars)
- Flag badges use `Badge` component (not inline spans)
- Avatar + username row compact (h-8 avatar, text-sm)
- Card hover: subtle shadow increase + border-highlight
- Action buttons (reanalyze) → `IconButton` component
### Dashboard Stats (`features/dashboard/components/DashboardStats.tsx`)
**Changes:**
- 4 stat cards: Total, Clean, Flagged, Error — using semantic colors
- `StatCard` component (internal or shared) with `variant` prop:
- `variant="primary"` → icon + accent in primary color
- `variant="success"` → icon + accent in success color
- `variant="destructive"` → icon + accent in destructive color
- `variant="warning"` → icon + accent in warning color
- Replace all hardcoded `emerald-*`, `blue-*`, `violet-*` with variant classes
- Grid: `sm:grid-cols-2 lg:grid-cols-4 gap-4`
- Animation: stagger with `cardStagger`
### UserSummaryList (`features/dashboard/components/UserSummaryList.tsx`)
**Changes:**
- Each user card: avatar + name + message count + status badge
- Status badges use semantic colors (clean → success, flagged → destructive)
- List item pattern with hover state
- Search input: `variant="soft"`
- Infinite scroll / pagination consistent
### UserProfileDetail (`features/dashboard/components/UserProfileDetail.tsx`)
**Changes:**
- All hardcoded `emerald-*`, `red-*`, `amber-*` → semantic CSS vars
- Detail sections with consistent spacing (`space-y-6`)
- Back button → `IconButton` with arrow-left
- Loading state → `Skeleton` with matching layout
- Error state → `EmptyState` with retry action
### ChannelProfileDetail (same pattern as UserProfileDetail)
### VoiceConnectionCard (`features/live/components/VoiceConnectionCard.tsx`)
**Changes:**
- Compact layout: guild select + channel select + connect button in one row
- Select dropdowns: `variant="soft"`
- Status indicator: `Badge` with connected/disconnected state
- Mic level meter: uses `--primary` as bar gradient
### AudioVisualizer (`features/live/components/AudioVisualizer.tsx`)
**Changes:**
- Read `--primary` CSS variable at paint time instead of hardcoded `#23a1eb`
- ```ts
const primaryColor = getComputedStyle(document.documentElement)
.getPropertyValue('--primary').trim();
```
- Bar color adapts to theme (lighter in dark mode)
- Responsive height: `h-32 md:h-40`
### ActiveSpeakers (`features/live/components/ActiveSpeakers.tsx`)
**Changes:**
- Replace hardcoded `text-emerald-700` with `text-success`
- Stacked avatar + name + speaking indicator dot
- Speaking: `bg-success` dot with pulse animation
- Idle: `bg-outline` dot
- Empty state: "No active speakers" with `EmptyState` component
### RecordingsSubPanel (`features/live/components/RecordingsSubPanel.tsx`)
**Changes:**
- Replace `bg-white` → `bg-surface` (CSS var)
- Replace `border-sky-200` → `border-outline-variant`
- Recording item layout: icon + filename + size + date + action button
### MessagesPanel Search & Filters
**Changes:**
- Search input: `variant="soft"`, debounced input (300ms)
- Filter pills: `Tabs variant="pills"` with `[All] [Clean] [Flagged] [Error] [Pending]`
- Stat badges row at top: uses `Badge` with semantic variants (not inline hardcoded spans)
---
## Layer 4: Polish & Micro-interactions
### Entry Animations
| Element | Animation | Timing | Source |
|---------|-----------|--------|--------|
| Page/panel enter | `fadeSlideUp` | 0.5s, [0.25,0.46,0.45,0.94] | `useFramerStagger` |
| Card grid enter | `cardStagger` + `cardItem` | stagger 80ms, item 400ms | `useFramerStagger` |
| List items enter | `cardStagger` + `cardItem` | stagger 50ms, item 300ms | `useFramerStagger` |
| Modal/dialog enter | `scaleIn` | 0.3s, backOut spring | `useFramerStagger` |
| Toast enter | slideInRight | 0.3s ease-out | Inline |
| Theme switch | CSS transition | 200ms background, 150ms color | CSS global |
| Sidebar collapse/expand | Spring physics | stiffness 300, damping 30 | Inline (existing) |
### Loading States
All skeleton components use consistent `animate-shimmer` with `1.5s ease-in-out` (CSS-canonical):
| Component | Skeleton Pattern | Width/Height |
|-----------|-----------------|--------------|
| MessageCard | Card shape: 3 lines + header blob | Full card |
| StatCard | Rectangular blob + icon circle | h-24 |
| User list item | Avatar circle + 2 text lines | h-14 |
| Detail view | Sidebar + header + content blobs | Full view |
### Empty States
| Context | Title | Description | Action |
|---------|-------|-------------|--------|
| Messages (no data) | "Belum Ada Pesan" | "Tunggu aktivitas di server. Pesan akan muncul secara real-time." | — |
| Messages (no search results) | "Tidak Ditemukan" | "Tidak ada pesan yang cocok dengan pencarianmu." | [Clear Search] |
| Voice (disconnected) | "Belum Connect" | "Pilih guild dan channel voice, lalu klik Connect." | — |
| Voice (no speakers) | "Sunyi Senyap" | "Belum ada yang speak di channel ini." | — |
| Dashboard (no data) | "Data Belum Siap" | "Data guild akan muncul setelah gateway aktif." | [Refresh] |
| Dashboard (no users) | "Belum Ada Pengguna" | "Data pengguna akan terkumpul seiring aktivitas server." | — |
| Recordings (empty) | "Belum Ada Rekaman" | "Rekaman voice akan muncul setelah ada sesi voice." | — |
### Toast Notification System
- **Position**: `fixed top-4 right-4 z-40` (not bottom — avoids MobileTabBar collision)
- **Stack**: gap-2, newest at top, max 5 visible
- **Types**: `info`, `success`, `error`, `warning` — all using semantic CSS vars
- **Auto-dismiss**: 4s (existing), with close button
- **Entry**: slide from right
- **Exit**: fade out + slide right
- **Grouping**: consecutive same-type toasts from same source merge into one
### Theme Toggle
- **Location**: Header (right side, near status badges)
- **Icon**: Sun/Moon from Lucide, animated (rotate + scale on switch)
- **Default**: Follow `prefers-color-scheme` media query
- **Persistence**: `localStorage.setItem('theme', 'light' | 'dark' | 'system')`
- **DOM attribute**: `document.documentElement.dataset.theme`
- **Transition**: CSS `transition` on `body` and card surfaces for smooth cross-fade
### Mascot Chatbot
- **Toggle**: Sidebar bottom button (existing) — playful bounce on click
- **Panel**: Floating card (320px wide) anchored to sidebar, not `z-[9999]`
- **Chat bubble**: Rounded-xl with primary-soft background, subtle shadow
- **Input**: `variant="soft"`, compact, with send icon
- **Empty state**: Mascot greeting in IMPHNEN voice: "Hai! Ada yang bisa dibantu?"
- **Error state**: "Wah, lagi error nih. Coba lagi ya!"
- **Reduced motion**: Chatbot itself has no animation, only fade mount
### Hover & Active States
| Element | Hover | Active | Transition |
|---------|-------|--------|------------|
| Sidebar nav item | `bg-primary-soft/50` | `bg-primary-soft` | 200ms |
| Card | `shadow-md border-primary/20` | — | 300ms |
| Button | Brightness/shift | `scale-[0.97]` | 150ms |
| List item | `bg-surface-container-high` | — | 150ms |
| Badge | Cursor pointer (if clickable) | — | — |
| Input | `border-hover` | `border-primary + ring` | 150ms |
---
## Spec Self-Review
### Placeholder Scan
- All sections above are complete with specific values (hex codes, component shapes, animation timings). No "TBD" or "TODO" remains.
### Internal Consistency
- Layer 0 (theme tokens) feeds directly into Layer 1 (components via CSS vars), Layer 2 (layout) uses same tokens. No contradiction.
- Dark palette maintains IMPHNEN's primary/secondary/tertiary distinction — secondary stays social-platform, tertiary stays Discord-specific.
- Z-index scale (z-10 through z-100) resolves existing collision between Toast (z-50) and MobileTabBar (z-50).
- Animation timings across layers: entry animations (0.30.5s), hover (150200ms), theme switch (150200ms) — all within IMPHNEN's "snappy" < 300ms promise.
### Scope Check
- Focused on visual redesign and brand cohesion of the existing React frontend.
- No changes to backend, API, database, or WebSocket protocol.
- No new features — all existing functionality is preserved, only the visual layer is redesigned.
- Scope is appropriate for a single implementation plan (see below).
### Ambiguity Check
- All design decisions are explicit: hex values, component variants, animation timings, layout measurements.
- The "ParticleBackground" behavior on mobile is specifically defined (skipped).
- Dark mode fallback is explicitly defined (system preference → manual toggle).
---
## Migration Sequence
The redesign will be implemented in order (Layer 0 → 4), with testing at each layer before proceeding:
1. **Layer 0** — Theme tokens, dark mode palette, CSS variable migration, fix shimmer mismatch, fix z-index
2. **Layer 1** — Badge (fix colors), Toast (rewrite), Skeleton (fix shimmer), new primitives (Switch, Dialog, EmptyState, IconButton)
3. **Layer 2** — Sidebar (expanded default), Header (simplified brand bar), Tab Strip (new), Mobile layout (overhaul), Particle (lazy)
4. **Layer 3** — MessageCard (refine), Dashboard (stat/User/Channel components), Live panels (AudioVisualizer, Speakers, Recordings)
5. **Layer 4** — Entry animations audit, empty/loading states, theme toggle, toast positioning, mascot polish, hover state consistency
Each layer is tested in both light and dark modes before proceeding to the next.
---
## Appendix: Feature Panel Map
| File | Layer | Priority | Changes |
|------|-------|----------|---------|
| `tailwind.config.js` | 0 | P0 | Remove duplicate tokens, keep only what `@theme` can't cover |
| `styles.css` | 0 | P0 | Add dark mode `:root` overrides, make canonical |
| `DESIGN_TOKENS.md` | 0 | P0 | Update to reflect changes, remove resolved issues |
| `shared/ui/badge.tsx` | 1 | P0 | Fix hardcoded success/warning colors |
| `shared/ui/toast.tsx` | 1 | P0 | Rewrite with CSS vars, fix z-index, change position |
| `shared/ui/skeleton.tsx` | 1 | P0 | Fix shimmer duration, add variants |
| `shared/ui/button.tsx` | 1 | P1 | Add tertiary variant, icon-sm size |
| `shared/ui/card.tsx` | 1 | P1 | Add elevated/bordered variants |
| `shared/ui/input.tsx` | 1 | P1 | Add soft variant |
| `shared/ui/empty-state.tsx` | 1 | P0 | New component |
| `shared/ui/switch.tsx` | 1 | P2 | New component |
| `shared/ui/dialog.tsx` | 1 | P2 | New component |
| `shared/ui/icon-button.tsx` | 1 | P1 | New component |
| `widgets/Sidebar.tsx` | 2 | P0 | Expanded default, better brand display |
| `widgets/Header.tsx` | 2 | P0 | Simplified brand bar, theme toggle |
| `widgets/DashboardLayout.tsx` | 2 | P0 | Add TabStrip, adjust layout |
| `widgets/TabStrip.tsx` | 2 | P0 | New component |
| `widgets/MobileTabBar.tsx` | 2 | P1 | Refine bottom nav, add safe area |
| `widgets/ParticleBackground.tsx` | 2 | P1 | Lazy render, mobile skip, CSS var colors |
| `features/messages/components/MessageCard.tsx` | 3 | P0 | Fix all hardcoded colors, use Badge |
| `features/messages/components/MessagesPanel.tsx` | 3 | P0 | Fix hardcoded stat badges |
| `features/messages/components/ImageGrid.tsx` | 3 | P1 | Fix hardcoded colors |
| `features/dashboard/components/DashboardStats.tsx` | 3 | P0 | Semantic color cleanup |
| `features/dashboard/components/UserSummaryList.tsx` | 3 | P0 | Fix hardcoded colors |
| `features/dashboard/components/UserProfileDetail.tsx` | 3 | P0 | Fix hardcoded colors |
| `features/dashboard/components/ChannelProfileDetail.tsx` | 3 | P1 | Fix hardcoded colors |
| `features/live/components/AudioVisualizer.tsx` | 3 | P0 | Read CSS var, not hardcoded hex |
| `features/live/components/ActiveSpeakers.tsx` | 3 | P1 | Fix hardcoded emerald |
| `features/live/components/RecordingsSubPanel.tsx` | 3 | P1 | Fix bg-white, fix sky-200 |
| `features/live/components/VoiceConnectionCard.tsx` | 3 | P2 | Compact layout |
| `features/auth/index.tsx` | 2 | P1 | Use EmptyState, consistent spacing |
| `App.tsx` | 2 | P0 | Add TabStrip state, theme toggle |
| All feature hooks | 3 | P2 | No visual changes |
@@ -1,311 +0,0 @@
# Leptos Rewrite — Frontend Architecture Design
**Date:** 2026-07-03
**Status:** Approved Design
## Overview
Rewrite the React 19 + Vite + Tailwind dashboard (46 source files, ~6k LOC) to a Rust Leptos CSR WASM app. All layers — components, styling, animations, WebSocket, audio, icons — are rewritten in Rust.
## Motivation
- **Performance** — WASM eliminates JS bundle parsing/execution overhead
- **Type Safety** — Rust's type system catches more errors at compile time
- **Bundle Size** — WASM binary smaller than equivalent JS bundle
## Status
The current React frontend (`services/frontend/`) remains in active use. The new Leptos app lives in `services/frontend-leptos/`. No React code is removed until the Leptos version is feature-complete.
## Tech Stack
| Layer | Choice | Rationale |
|-------|--------|-----------|
| Framework | Leptos 0.7 (CSR) | WASM-native reactive framework, signals-based |
| Styling | Plain CSS | No framework dependency, port from existing CSS |
| Animations | CSS keyframes + transitions | Replace all Framer Motion usage |
| Icons | `lucide-leptos` | Direct port of current lucide-react icons |
| HTTP | `gloo-net` | Rust fetch wrapper, works with WASM |
| WS | `web-sys::WebSocket` | Direct binding, no abstraction overhead |
| Audio Viz | Canvas 2D (`web-sys`) | AudioVisualizer, WaveformPlayer |
| Audio Playback | Web Audio API (`web-sys`) | PCM stream playback |
| State | Leptos signals + contexts | No external state library needed |
| Build | `trunk` | Standard WASM builder for Leptos |
## Architecture
```
Leptos App (WASM)
├── App Shell (layout, sidebar, header, tabs)
├── Messages Panel (feed, cards, search, filters)
├── Live Panel (voice, music, screen, recordings, audio viz)
└── Dashboard Panel (stats, users, channels)
Shared infrastructure:
├── WebSocket Context (singleton + per-event channels)
├── API Client (fetch wrapper + typed endpoint functions)
├── UI Primitives (Button, Card, Badge, Input, Tabs, Toast, etc.)
└── CSS Design System (custom properties, component classes, animations)
```
### Workspace Structure
```
services/frontend-leptos/
├── Cargo.toml # workspace root
├── shared-types/ # Rust types (port of @bete/shared)
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs
│ ├── message.rs # MessageRecord, AttachmentRecord
│ ├── guild.rs # Guild, Channel
│ ├── voice.rs # VoiceStatus, ActiveSpeaker
│ ├── media.rs # MediaItem, MediaState, MediaMode
│ ├── dashboard.rs # DashboardStats, DashboardUser, DashboardChannel
│ ├── recording.rs # VoiceRecording
│ └── ui_state.rs # UIState, AppConfig
├── frontend/
│ ├── Cargo.toml # Leptos + deps
│ ├── Trunk.toml # Build config
│ ├── index.html # Entry point (lang="id", Poppins font)
│ └── src/
│ ├── main.rs # mount_to_body with panic_hook + logger
│ ├── app.rs # Root component (providers + auth gate + routing)
│ ├── app.css # Full CSS design system
│ ├── lib.rs # Module declarations
│ ├── auth.rs # AuthOverlay + login logic
│ ├── ws/ # WebSocket context & event system
│ ├── api/ # HTTP client + typed API functions
│ ├── ui/ # Shared UI primitives
│ ├── layout/ # App shell components
│ └── features/
│ ├── messages/ # Message feed, cards, search, filters
│ ├── live/ # Voice, audio, media, recordings
│ └── dashboard/ # Stats, users, channels
├── rust-toolchain.toml # nightly (required by Leptos CSR)
└── .env # API_URL, WS_URL
```
### Component Tree
```
<App>
<ToastProvider>
{!authenticated && !is_public → <AuthOverlay />}
<DashboardLayout>
<ParticleBackground />
<Sidebar>
<NavLinks> Messages | Live | Dashboard </NavLinks>
<MascotImage on:click → toggle MascotChatbot />
</Sidebar>
<Header>
<Logo />
<ConnectionIndicator />
<ThemeToggle />
</Header>
<TabStrip />
{active_tab == "messages" → <MessagesPanel />}
{active_tab == "live" → <LivePanel />}
{active_tab == "dashboard" → <DashboardPanel />}
</DashboardLayout>
<MascotChatbot />
</ToastProvider>
</App>
```
### State Architecture
**Contexts** (provided at `<App>` level via `provide_context`):
| Context | Key Signals | Persisted |
|---------|-------------|-----------|
| AuthContext | `authenticated: ReadSignal<bool>` | sessionStorage |
| WsContext | `status: ReadSignal<WsStatus>`, per-event channels | — |
| UiContext | `active_tab: RwSignal<Tab>`, `selected_guild: RwSignal<Option<String>>`, etc. | localStorage |
**Data fetching** via Leptos `Resource` (parallel to React useEffect + fetch, with built-in loading/error/ok states).
**WebSocket events** dispatched through per-event-type `mpsc::UnboundedReceiver` channels. Components subscribe to relevant channels via `watch!` or `create_effect`.
## State Machine: Tab Navigation
```
[messages] ←── [live] ←──→ [dashboard]
| ↑
└─── (unauth redirect) ──┘
```
- Default: "messages"
- Unauthenticated on "live" → redirect to "messages" (same as current behavior)
- Tab state persisted via localStorage
## Data Flow
### Message Capture → Display
```
Discord → gateway → Redis → backend → WS → ws::socket receive
→ parse JSON → match event type → WsContext.on_message_created.send(data)
→ watch! { set_messages.update(|msgs| merge(msgs, data)) }
→ UI re-renders via Leptos reactivity
```
### Voice PCM → Audio Playback
```
Discord → gateway → Redis → backend → WS binary frame
→ ws::socket onbinary → parse [u32be userId][i16 samples]
→ WsContext.on_voice_pcm.send(packet)
→ use_audio_playback → AudioContext.decodeAudioData → play via AudioBufferSourceNode
→ AudioVisualizer: CanvasRenderingContext2D draw 32 bars on requestAnimationFrame
```
### User Action → Command
```
Button click → call api::voice_connect(guild_id, channel_id)
→ POST /api/voice/connect
→ backend → Redis (backend:command) → discord-gateway
→ gateway connects → Redis (discord:voice:started)
→ WS event → komponen update status
```
## Phase Breakdown
### Phase 1: Foundation (project scaffold + types + build)
- [ ] Cargo workspace with `shared-types` and `frontend` crates
- [ ] All Rust types (port 1:1 from TypeScript types + `@bete/shared`)
- [ ] `Trunk.toml` + `index.html` + `main.rs` with `mount_to_body`
- [ ] `rust-toolchain.toml` (nightly)
- [ ] `.env` for API/WS URLs
- [ ] Build verification: `trunk serve` produces a blank WASM page
### Phase 2: Skeleton (shell, auth, WS, API, UI primitives)
- [ ] CSS Design System (`app.css`) — custom properties, component classes, keyframes, dark theme
- [ ] `<App>` component with providers
- [ ] AuthOverlay + login flow
- [ ] WebSocket singleton (connect, reconnect with exponential backoff, per-event channels)
- [ ] API client (fetch wrapper with auth header, typed endpoint functions)
- [ ] UI primitives: Button (7 variants), Badge (9), Card, Input, Select, Tabs, ScrollArea, Toast, Skeleton, StatusBadge, EmptyState, Modal
- [ ] Layout shell: DashboardLayout, Sidebar, Header, TabStrip, MobileTabBar
### Phase 3: Messages Feature
- [ ] MessagesPanel with AI status filter tabs
- [ ] MessageFeed (infinite scroll via IntersectionObserver)
- [ ] MessageCard with user grouping
- [ ] MessageRow (content, edited/deleted indicators, Discord emoji, stickers, attachments, AI analysis box, severity badge, reanalyze)
- [ ] ImageGrid (masonry grid from attachments/embeds/stickers)
- [ ] ModerationAlertListener → Toast dispatch
- [ ] Search (via `/api/analysis/search`)
- [ ] Reanalyze (single + batch)
### Phase 4: Live Feature
- [ ] VoiceConnectionCard (guild/channel selectors, join/disconnect/listen/transmit)
- [ ] ActiveSpeakers (user list with speaking indicators)
- [ ] AudioVisualizer (Canvas 2D, 32 bars, gradient, ResizeObserver)
- [ ] MicLevelMeter (horizontal bar, 0-100%, green→red scale)
- [ ] NowPlaying (current media + queue)
- [ ] MusicSubPanel (URL input, volume slider, queue/skip/stop)
- [ ] ScreenSubPanel (URL input, start/skip/stop)
- [ ] RecordingsSubPanel (list + pagination + delete + status badges)
- [ ] WaveformPlayer (Canvas 64 bars, AudioContext, play/pause/seek)
- [ ] PCM audio playback (Web Audio API via web-sys)
- [ ] Mic transmit (AudioContext → WebSocket binary frames)
### Phase 5: Dashboard Feature
- [ ] StatsOverview (8 stat cards, top channels, moderation queue)
- [ ] UserSummaryList (search + pagination + grid)
- [ ] UserProfileDetail (stats + recent messages)
- [ ] ChannelSummaryList (search + pagination + grid)
- [ ] ChannelProfileDetail (stats + culture + recent messages)
### Phase 6: Polish
- [ ] ParticleBackground (3 CSS glow orbs)
- [ ] MascotChatbot (floating chat panel, API integration)
- [ ] MascotImage (clickable mascot)
- [ ] Edge cases: WS reconnect, stale data handling, concurrent requests
- [ ] Mobile responsive (< md: sidebar → bottom nav, cards → single column)
- [ ] Loading states + error boundaries + empty states for all panels
- [ ] Theme toggle (dark/light via CSS custom properties)
- [ ] Error animation states (Framer Motion fade/slide ported to CSS keyframes)
## API Client
All existing endpoints mapped to typed Rust functions:
```rust
// pattern
pub async fn get_messages(guild_id: &str, params: &MessageParams) -> Result<PageResult<MessageRecord>, ApiError>;
// All endpoints (20+):
// auth, messages, review, reanalyze, guilds, voice, media, recordings,
// dashboard (stats/users/channels), ui-state, chat, search
```
Full list in `services/frontend/src/shared/api/client.ts` — 1:1 port.
## CSS Design System
### Source: `services/frontend/src/styles.css` (676 lines)
**Port strategy:**
1. CSS custom properties (`:root` / `[data-theme="dark"]`) — port verbatim
2. Component classes (`.im-btn`, `.im-card`, etc.) — rename to `.btn`, `.card` etc., port styling
3. Keyframes — rename from `.im-*` prefix, port verbatim
4. Layout utility classes (`.flex`, `.grid`, `.gap-*`) — keep as utility classes or inline
5. Remove all Tailwind-specific directives
**Component classes to define:**
- `.btn` / `.btn-primary` / `.btn-secondary` / `.btn-ghost` / `.btn-destructive` / `.btn-outline` / `.btn-link` + `.btn-sm` / `.btn-lg` / `.btn-icon`
- `.card` / `.card-elevated` / `.card-bordered` + `.card-header` / `.card-title` / `.card-content` / `.card-footer`
- `.badge` / `.badge-primary` / `.badge-success` / `.badge-warning` / `.badge-destructive` / `.badge-outline`
- `.input` / `.input-soft` + `.input-error`
- `.tabs` / `.tab-list` / `.tab-trigger` / `.tab-content`
- `.skeleton` / `.skeleton-circular` / `.skeleton-rectangular`
- `.toast` / `.toast-success` / `.toast-error` / `.toast-warning`
## WebSocket Protocol (unchanged from current)
- **Binary**: PCM audio `[4-byte userIdHash UInt32LE][Int16 PCM samples @ 24kHz mono]`
- **JSON**: Typed envelope `{ type, data }` — 20+ event types (see `events.md`)
Reconnection: exponential backoff with full jitter (1s base, 30s max, 20 attempts).
## Out of Scope (Phase 1)
- React Native / mobile apps
- PWA / service worker
- E2E testing
- Performance benchmarking
- Bundle size optimization
- Server-side rendering (Leptos SSR mode)
- CI/CD integration
These can be added after the CSR WASM version is stable.
## Dependencies
### Rust crates
| Crate | Version | Purpose |
|-------|---------|---------|
| leptos | 0.7 (csr) | Reactive UI framework |
| leptos-use | latest | `use_local_storage`, `use_interval`, `use_event_listener` etc. |
| lucide-leptos | latest | SVG icons |
| wasm-bindgen | 0.2 | JS/WASM bindings |
| web-sys | 0.3 | Browser API bindings |
| js-sys | 0.3 | JS type bindings |
| gloo-net | 0.6 | HTTP fetch |
| serde | 1 + derive | JSON serialization |
| serde-wasm-bindgen | 0.6 | Serde ↔ JS interop |
| wasm-logger | 0.2 | Logging to console |
| console_error_panic_hook | 0.1 | Debug panic traces |
### web-sys features required
WebSocket, CanvasRenderingContext2d, AudioContext, Window, Document, Element, HtmlElement, KeyboardEvent, Storage, IntersectionObserver, ResizeObserver, Url, Headers, Request, RequestInit, Response, HtmlInputElement, HtmlAudioElement, HtmlCanvasElement, MediaDevices, MediaStream, AudioBuffer, AudioBufferSourceNode
## Migration Notes
- The React app (`services/frontend/`) stays intact until Leptos version is complete
- No shared code between React and Leptos versions — complete rewrite
- Build output: `trunk build``dist/` folder, deployable as static files alongside or replacing the current Vite build
- Environment variables: port from `.env` to Rust compile-time config or runtime JS interop
@@ -1,92 +0,0 @@
# Frontend dependency upgrade design
Date: 2026-07-04
## Goal
Upgrade the Bete frontend to the newest feasible dependency and build-tool surface, including pre-release versions when they can be verified. The upgrade should preserve dashboard behavior and leave the repository in a buildable state.
## Scope
The frontend upgrade covers the full Rust/WASM frontend build surface:
- `services/frontend/frontend/Cargo.toml`
- `services/frontend/shared-types/Cargo.toml`
- `services/frontend/Cargo.lock`
- `services/frontend/rust-toolchain.toml`
- root frontend scripts when they need to reflect tool changes
- production frontend build path in `infra/docker/Dockerfile.proxy`
- CI integration in `.gitlab-ci.yml` only if required by the Docker/build changes
The upgrade does not include UI redesign, feature changes, backend API changes, or unrelated refactors.
## Target dependency policy
Use a "max feasible" policy:
1. Attempt the newest visible releases, including pre-releases, for the main frontend stack.
2. Prefer the newest version that passes verification over forcing a broken latest version.
3. If a pre-release blocks compilation or requires migration work outside this upgrade's scope, pin the newest passing version and document the blocker.
Initial target versions discovered during design:
- `leptos = "0.9.0-alpha"`
- `leptos-use = "0.19"`
- `lucide-leptos = "3.23"`
- `trunk = "0.22.0-beta.1"`
Support crates such as `wasm-bindgen`, `wasm-bindgen-futures`, `web-sys`, `js-sys`, `serde`, `serde_json`, `serde-wasm-bindgen`, `gloo-net`, `gloo-timers`, `wasm-logger`, `console_error_panic_hook`, and `regex` should be updated through Cargo resolution unless direct manifest changes are needed.
## Toolchain and production build alignment
The current frontend toolchain is pinned to `nightly-2026-06-01`. The latest Trunk beta advertises a Rust requirement of `1.90.0`, so the toolchain must be checked and raised if needed.
The production proxy image currently runs:
```dockerfile
RUN cargo install trunk --locked
```
That is non-deterministic over time because it installs whatever `trunk` is latest when the image is built. The upgrade should make this deterministic, preferably by pinning the intended Trunk version explicitly:
```dockerfile
RUN cargo install trunk --version 0.22.0-beta.1 --locked
```
If the beta package fails with `--locked`, the implementation may either adjust the install command with a documented reason or fall back to the newest verified Trunk version.
## Migration strategy
1. Establish a baseline by running the existing frontend check/build commands before changing dependencies.
2. Upgrade build tooling first so local and Docker builds agree on Rust and Trunk versions.
3. Upgrade the main frontend crates in `Cargo.toml` and refresh `Cargo.lock`.
4. Fix compatibility errors caused by Leptos, Leptos-use, Lucide, or support-crate API changes.
5. Keep fixes behavior-preserving. Avoid UI redesign and broad refactoring.
6. If a dependency cannot be upgraded to the initial target, record the attempted version, the failure mode, and the chosen fallback.
## Verification plan
Run these checks after the upgrade:
1. `pnpm run typecheck:web`
2. `pnpm run build:web`
3. `pnpm run lint` if changed files are covered by Biome or root linting
4. `pnpm run test` if the changes affect packages with runnable tests; otherwise explicitly report that tests were skipped and why
5. Prefer `docker build -f infra/docker/Dockerfile.proxy .` to validate the production frontend build path
If Docker is unavailable or impractical in the environment, report that limitation and rely on the local Trunk release build as the minimum build verification.
## Success criteria
- The frontend Cargo workspace resolves cleanly.
- The WASM release build succeeds.
- The production proxy Dockerfile uses a deterministic Trunk/toolchain path or has a documented reason for any exception.
- No intentional dashboard behavior or visual design changes are introduced.
- Any dependency left below the newest attempted version has a clear documented reason.
## Risks and mitigations
- **Leptos alpha API churn:** Fix only compatibility issues required for compilation and runtime preservation. Roll back to the newest verified Leptos version if the migration becomes too broad.
- **Trunk beta toolchain requirement:** Align Rust toolchain and Docker install commands before validating the app build.
- **Local build passing while Docker fails:** Verify the proxy Dockerfile when possible because production serves the frontend from that image.
- **Unrelated churn:** Keep edits scoped to manifests, lockfile, build tooling, and compatibility changes directly caused by the upgrade.
@@ -1,397 +0,0 @@
# Frontend Overhaul — IMPHNEN Dashboard
**Date:** 2026-07-05
**Status:** Spec (approved design, pre-implementation)
---
## 1. Goals
1. **Visual redesign** — Elevate the dashboard from "functional prototype" to "premium monitoring tool" using the Neuform-inspired dark design language (AeroNet, Nexus Capital, Summit references from `design/`).
2. **Code restructure** — Split oversized files, modularize CSS, eliminate inline styles, and add consistent state handling across all components.
---
## 2. Scope
Two sequential phases, both targeting the Leptos 0.9.0-alpha WASM frontend at `services/frontend/frontend/`.
| Phase | Focus | Deliverables |
|-------|-------|-------------|
| **Fase 1** | Code Restructure | File splits, CSS modules, inline→classes, state consistency |
| **Fase 2** | Visual Redesign | Palette, layout, all panels, UI primitives |
---
## 3. File Splitting (Fase 1)
### 3.1 `message_card.rs` (453 lines → 4 files)
| New file | Content | Lines (est.) |
|----------|---------|-------------|
| `message_card.rs` | Main component — card layout orchestration | ~100 |
| `message_embed.rs` | Discord embed rendering (title, fields, images, footer) | ~120 |
| `message_meta.rs` | Author row, timestamp, channel badge, AI status chip | ~100 |
| `message_actions.rs` | Reanalyze button, moderate action, expand/collapse | ~80 |
### 3.2 `messages/mod.rs` (397 lines → 3 files)
| New file | Content | Lines (est.) |
|----------|---------|-------------|
| `mod.rs` | Panel orchestrator — wires FilterBar + MessageList | ~60 |
| `filter_bar.rs` | Channel picker, search input, status filter chips, live count | ~150 |
| `message_list.rs` | Infinite scroll logic, pagination, skeleton/empty/error states | ~180 |
### 3.3 `ws/socket.rs` (227 lines → 2 files)
| New file | Content | Lines (est.) |
|----------|---------|-------------|
| `connection.rs` | WebSocket lifecycle (connect, reconnect, heartbeat, close) | ~120 |
| `handlers.rs` | Event dispatch — maps WS event types to signal updates | ~100 |
### 3.4 Dashboard (`dashboard/mod.rs`, 276 lines)
Already well-split (`stats_overview.rs`, `channel_summary_list.rs`, `user_summary_list.rs` are separate). Extract orchestrator wiring from `mod.rs` into a focused panel component. No further splitting needed.
---
## 4. CSS Architecture (Fase 1)
### 4.1 File Structure
```
src/styles/
├── main.css @import hub — imported by index.html
├── tokens.css CSS custom properties (dark + light)
├── reset.css Reset, base elements, scrollbar, selection
├── utilities.css Utility classes (flex, gap, grid, w-full, etc.)
├── layout.css App shell, sidebar, topbar, content area, mobile tab
├── ui.css Button, card, input, select, modal, badge,
│ skeleton, tabs, scroll area, toast, status badge
├── messages.css Message card, feed, embed, image grid, filter bar
├── live.css Voice connection, speakers, visualizer, mic meter,
│ music player, screen share, recordings, waveform
├── dashboard.css Stats overview, channel/user summary lists
└── polish.css Particle background, theme toggle, mascot chatbot
```
### 4.2 Bundling
`index.html` links to `src/styles/main.css`. Trunk resolves `@import` statements and bundles into a single CSS file in the build output. Ordering is guaranteed by `@import` sequence.
### 4.3 Design Tokens (`tokens.css`)
#### Dark theme (default — `:root` without attribute selector)
```css
:root {
--surface-base: #050510;
--surface-raised: #0a0a1a;
--surface-overlay: #12122a;
--surface-container: #1a1a35;
--surface-border: rgba(255, 255, 255, 0.06);
--surface-hover: rgba(59, 130, 246, 0.06);
--surface-glass: rgba(5, 5, 16, 0.78);
--text-primary: #f1f1f9;
--text-secondary: #9d9db5;
--text-tertiary: #5c5c78;
--text-inverse: #050510;
--color-primary: #3b82f6;
--color-primary-hover: #60a5fa;
--color-primary-active: #2563eb;
--color-primary-muted: rgba(59, 130, 246, 0.12);
--gradient-primary: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%);
--gradient-brand: linear-gradient(135deg, #3b82f6 0%, #5865f2 50%, #6366f1 100%);
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-info: #3b82f6;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
--shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.4);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 23px;
--radius-pill: 9999px;
--sidebar-width: 240px;
--header-height: 0px; /* header dihapus */
}
```
#### Light theme (`[data-theme="light"]`)
```css
[data-theme="light"] {
--surface-base: #f4f6fb;
--surface-raised: #ffffff;
--surface-overlay: #eeeff4;
--surface-container: #dde0e8;
--surface-border: rgba(0, 0, 0, 0.06);
--surface-hover: rgba(35, 161, 235, 0.05);
--surface-glass: rgba(255, 255, 255, 0.72);
--text-primary: #0f172a;
--text-secondary: #475569;
--text-tertiary: #94a3b8;
--text-inverse: #ffffff;
--color-primary: #2563eb;
--color-primary-hover: #3b82f6;
--color-primary-active: #1d4ed8;
--color-primary-muted: rgba(37, 99, 235, 0.1);
--gradient-primary: linear-gradient(135deg, #2563eb 0%, #6366f1 100%);
--gradient-brand: linear-gradient(135deg, #2563eb 0%, #5865f2 50%, #6366f1 100%);
/* reuse same semantic tokens */
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
--color-info: #2563eb;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.08);
}
```
---
## 5. Layout Architecture (Fase 2)
### 5.1 Current → Proposed
```
// Current (app.rs)
app-shell
├── app-header (brand + ThemeToggle + WS status — redundant)
├── app-main
│ ├── app-sidebar (TabButton × 3 — no icons, inline styles)
│ └── app-content (tab panel)
// Proposed
app-shell
├── app-sidebar
│ ├── brand (◉ IMPHNEN + "Guild Watcher" subtitle)
│ ├── nav (3 items with lucide icons + active indicator)
│ └── footer (WS status dot + compact ThemeToggle)
└── app-content
└── tab-panel (no header wrapper — full height)
```
### 5.2 Sidebar Specifics
- **Width:** 240px, full viewport height, sticky/fixed
- **Brand area:** 60px tall, logo placeholder (◉) + "IMPHNEN" bold + subtitle "Guild Watcher" (JetBrains Mono 11px)
- **Nav items:** icon (lucide-leptos) + label, 40px tall, border-radius 10px on hover/active
- Active state: 3px left border indicator + `var(--surface-overlay)` background
- **Footer:** separated by a subtle divider, WS dot + status text + ThemeToggle icon-button
- **Mobile:** sidebar collapses or slides overlay, MobileTabBar at bottom
### 5.3 Content Area
- Full remaining width, scrollable, padding 24px (desktop)
- Max-width 1400px for readability
- No header at top — brand is in sidebar
---
## 6. Components Redesign (Fase 2)
### 6.1 Messages Panel
**FilterBar:**
- **Channel picker:** `<select>` redesigned — card-style dropdown with active channel name displayed, chevron icon
- **Search:** input with magnifying glass icon (lucide), compact height (36px)
- **Status chips:** pill-style toggle buttons — "All", "Clean", "Warn", "Flagged", "Error" — active chip gets primary background
- **Live count:** `--text-tertiary` label "X,XXX pesan" next to heading
**MessageCard:**
```
┌─────────────────────────────────────────────┐
@username · #channel · 12:34 ◉ │ ← meta row
├─────────────────────────────────────────────┤
│ Message content — Inter 14px, │
│ line-height 1.5, wraps naturally │
│ │
│ [Embed card-in-card] │
│ ┌───────────────────────────────────────┐ │
│ │ Embed title (semibold) │ │
│ │ Embed description │ │
│ │ [thumbnail] │ │
│ └───────────────────────────────────────┘ │
├─────────────────────────────────────────────┤
│ ● Clean 98% [⟳] [⚙] │ ← actions bar
└─────────────────────────────────────────────┘
```
- Card: `surface-raised`, border `1px solid border`, radius `--radius-md`
- Meta row: JetBrains Mono 12px, `text-secondary`
- AI badge: chip dengan soft background (`--color-primary-muted` atau `--color-success` variant), 6px radius
- Content: Inter 14px, max 6 lines with fade gradient overflow
- Actions bar: icon-only buttons, visible always (not on hover)
- Timestamp: relative (baru dimodalin — "2m ago", "1h ago")
- Shadow: `--shadow-sm`
**States:**
| State | Component |
|-------|-----------|
| Loading | 3-5 skeleton cards, each with pulse animation (gradient shimmer) |
| Empty | Icon + "Belum ada pesan" + "Coba ubah filter atau channel" |
| Error | Error icon + error message + "Coba lagi" button |
| Success | Normal message list |
### 6.2 Live Panel (Bento Grid)
2-column grid with `gap: --space-4`:
```
┌──────────────────────┬──────────────────────┐
│ VoiceConnectionCard │ ActiveSpeakers │
├──────────────────────┼──────────────────────┤
│ AudioVisualizer │ MicLevelMeter │
├──────────────────────┴──────────────────────┤
│ MusicPlayer (NowPlaying + controls) │
├──────────────────────┬──────────────────────┤
│ ScreenSubPanel │ RecordingsSubPanel │
└──────────────────────┴──────────────────────┘
```
**VoiceConnectionCard:**
- Shows channel name, connection status (dot + text), connect/disconnect button
- States: disconnected (picker CTA), connecting (pulse), connected (info)
**AudioVisualizer + MicLevelMeter:**
- Canvas 2D with glass card background
- Waveform rendering yang lebih smooth
**NowPlaying / MusicPlayer:**
- Track info (thumbnail/icon + title + artist)
- Progress bar + time
- Controls: prev/play-pause/next + volume + stop
**States:**
| State | Display |
|-------|---------|
| No voice connected | CTA card: "Connect to a voice channel" with guild/channel picker |
| Connecting | Animated pulse on status indicator |
| Connected, no speakers | Empty state: "Waiting for speakers..." |
| Music idle | "No track playing" + queue button |
| All normal | Full grid |
### 6.3 Dashboard Panel
**StatsOverview (4-column grid):**
```
┌──────────┬──────────┬──────────┬──────────┐
│ Metrics │ Metrics │ Metrics │ Metrics │
│ 12,458 │ 3,201 │ 89 │ 24 │
│ Messages │ Users │ Channels │ Flagged │
│ +12% │ Active │ Total │ ↑3 │
└──────────┴──────────┴──────────┴──────────┘
```
- Each stat card: 140px tall, number in JetBrains Mono 28px bold, label in Inter 12px secondary, trend line in 11px
- Trend: green for positive/neutral, red for negative
**ChannelSummaryList / UserSummaryList:**
- Row cards (border-bottom yang subtle, hover background)
- Channel: `#` icon + name + message count + last active tooltip
- User: avatar initial circle + username + message count + trust score badge
- Loading: 5 skeleton rows
- Empty: "Belum ada data"
---
## 7. UI Primitives Upgrade (Fase 2)
| Component | Upgrade |
|-----------|---------|
| **Button** | Pill radius (`--radius-pill`), icon slot via `children`, loading spinner overlay, hover lift (translateY -1px + shadow-md), variants: primary/ghost/destructive/icon |
| **Card** | `--radius-md`, border `1px solid border`, shadow-sm, optional interactive hover (shadow-md + border-primary) |
| **Badge** | `--radius-sm`, soft background (opacity variant), compact padding 4px 8px |
| **Skeleton** | Gradient shimmer (base → highlight → base sliding), `--radius-sm` |
| **Modal** | Backdrop blur (8px), enter transition (scale 0.95→1 + fade), exit reverse |
| **Input** | --radius-sm, focus ring 2px `--color-primary` + subtle glow, border `1px solid border` → primary on focus |
| **Select** | Same as input, custom chevron icon |
| **Toast** | Fixed bottom-right, enter slide-in right, progress bar for auto-dismiss, variants: success/error/info |
| **Tabs** | Underline indicator (2px primary), no fill |
---
## 8. State Handling Convention
Every data-fetching component MUST handle these four states:
```rust
// Pattern (pseudocode)
match loading, data, error {
(true, _, _) => render_skeleton(),
(false, Some(data), _) => render_success(data),
(false, None, Some(err)) => render_error(err, retry_fn),
(false, None, None) => render_empty(empty_msg, icon),
}
```
List of components requiring state audit:
- `MessageList` (messages) — skeleton needed, empty/error exist?
- `VoiceConnectionCard` (live) — exists partially
- `ActiveSpeakers` (live) — empty state needed
- `RecordingsSubPanel` (live) — loading/empty needed
- `MusicSubPanel` (live) — idle/loading needed
- `StatsOverview` (dashboard) — skeleton needed
- `ChannelSummaryList` (dashboard) — loading/empty needed
- `UserSummaryList` (dashboard) — loading/empty needed
- `ImageGrid` (messages) — loading/empty needed
- `MascotChatbot` (polish) — loading needed
---
## 9. Inline Styles → CSS Classes (Fase 1)
Files with `style=` props that must be extracted:
| File | Inline styles | Action |
|------|--------------|--------|
| `sidebar.rs` | `style:width`, `style:background`, `style:color` on NavItem | Extract to `.sidebar`, `.nav-item`, `.nav-item.is-active` |
| `header.rs` (being removed) | `style:` for header layout, status dot | Style moves to layout.css |
| `mobile_tab_bar.rs` | `style:color`, flex on buttons | Extract to `.mobile-tab-bar`, `.mobile-tab-item` |
| `auth.rs` | Inline button style, close button | Extract to `.auth-box`, `.auth-close-btn` |
---
## 10. Implementation Order
### Fase 1 — Code Restructure (dilakukan duluan biar Fase 2 punya fondasi bersih)
1. CSS modular: buat `src/styles/` dan pindahkan CSS dari `app.css`
2. File splitting: `message_card.rs` → 4 files, `messages/mod.rs` → 3 files, `ws/socket.rs` → 2 files
3. Inline style → CSS classes: sidebar, mobile_tab_bar, auth
4. State audit: tambah loading/empty/error states ke komponen yang kurang
5. Dead code cleanup, verify build still works
### Fase 2 — Visual Redesign
1. Update design tokens (`tokens.css`) — new palette, dark default
2. Layout restructure: hapus header, rebuild sidebar as primary nav
3. UI primitives upgrade (button, card, modal, etc.)
4. Messages Panel redesign (FilterBar, MessageCard)
5. Live Panel redesign (bento grid)
6. Dashboard Panel redesign (stat cards, row lists)
7. Polish: transitions, micro-interactions, responsive
---
## 11. Out of Scope
- Leptos version upgrade (staying on 0.9.0-alpha)
- New features or panels (pure overhaul of existing surface)
- Backend changes
- Performance optimization beyond CSS/rendering
- Test coverage (separate effort)
@@ -1,104 +0,0 @@
# Gitea Migration and CI/CD Design
## Goal
Migrate this project to the self-hosted Gitea instance at `https://git.imrnes.team` under `MythEclipse/GMW`, then add a Gitea Actions deployment workflow that behaves like the existing CI/CD setup while using Gitea user-level secrets.
## Repository Target
- Owner: `MythEclipse`
- Repository: `GMW`
- SSH remote: `ssh://git@git.imrnes.team:22222/MythEclipse/GMW.git`
- Deployment branch: `main`
The local repository currently uses `master`, so the migration will publish the current HEAD as `main` for Gitea Actions.
## Existing Project Constraints
- Package manager: `pnpm@11.1.3`
- Root lint command: `pnpm run lint`
- Build commands:
- `pnpm run build:backend`
- `pnpm run build:discord-gateway`
- `pnpm run build:web`
- Frontend build uses Trunk/Rust from `services/frontend/frontend`.
- Deployment script already exists at `./deploy.sh` and performs bind-mounted hot deployment to `/opt/imphenbot` on the VPS.
- `.env` must not be committed.
## Chosen Approach
Use `deploy.sh` as the provider-neutral deploy entrypoint.
The Gitea workflow will perform CI locally in the runner, write required runtime files from user-level secrets, then call `./deploy.sh --no-build` so the deploy script only transfers already-built artifacts and restarts containers.
Rejected alternatives:
1. Building and pushing Docker images to Gitea packages. This is more invasive, requires registry/auth changes, and is unnecessary because the existing deploy script already supports bind-mounted artifact deployment.
2. CI-only without deployment. This is safer but does not satisfy the requested CI/CD migration.
## Workflow Design
Create `.gitea/workflows/deploy.yml` with:
- Trigger: push to `main`.
- Checkout with submodules.
- Setup Node and pnpm.
- Install dependencies with `pnpm install --frozen-lockfile`.
- Run `pnpm run lint`.
- Build backend, discord-gateway, and frontend.
- Write `${{ secrets.PRODUCTION_ENV }}` to `.env` with mode `600`.
- Write `${{ secrets.VPS_SSH_KEY }}` to a temporary private-key file with mode `600`.
- Export:
- `VPS_HOST=${{ secrets.VPS_HOST }}`
- `VPS_USER=${{ secrets.VPS_USER }}`
- `VPS_SSH_KEY=<temp key path>`
- Run `./deploy.sh --no-build`.
The workflow must not create repo-level secrets and must not print secret contents.
## `deploy.sh` Design
Update `deploy.sh` to remove GitLab-specific fallback behavior.
The script will:
- Read deployment inputs from environment variables.
- Require `VPS_HOST`, `VPS_USER`, and `VPS_SSH_KEY`.
- Accept `VPS_SSH_KEY` as either a path to an existing key file or raw private-key contents; if raw contents are supplied, write them to a temporary file and clean it up.
- Preserve existing local usage and service flags:
- `--frontend`
- `--backend`
- `--gateway`
- `--all`
- `--no-build`
- Keep the current bind-mount artifact deployment flow.
## Error Handling and Safety
- Missing required secrets/env vars should fail fast before deployment starts.
- Secret values must not be echoed.
- `.env` is generated only inside the workflow workspace and remains uncommitted.
- Deployment continues to use atomic remote directory swaps for artifacts.
- The script should not depend on GitHub, GitLab, or Gitea CLIs for deployment.
## Verification Plan
After implementation:
1. Verify lint passes with `pnpm run lint`.
2. Verify builds pass:
- `pnpm run build:backend`
- `pnpm run build:discord-gateway`
- `pnpm run build:web`
3. Verify `deploy.sh` help/syntax still works without requiring secrets.
4. Create `MythEclipse/GMW` in Gitea if it does not exist.
5. Set `origin` to `ssh://git@git.imrnes.team:22222/MythEclipse/GMW.git`.
6. Push `main` to Gitea.
7. Verify `git remote -v` uses the SSH Gitea URL with port `22222`.
8. Check Gitea Actions runs. If `tea actions runs list` is unsupported, use `tea api /repos/MythEclipse/GMW/actions/runs`.
## Out of Scope
- Creating duplicate repo-level secrets.
- Replacing the bind-mounted deploy model with a Docker registry deploy.
- Committing `.env` or exposing secret values in logs.