From 0f871026ebec8fbdfe949470b49b8799d2e7ee27 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Thu, 26 Mar 2026 17:08:23 +0100 Subject: [PATCH] docs: add impersonate panel design spec Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-26-impersonate-panel-design.md | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-26-impersonate-panel-design.md diff --git a/docs/superpowers/specs/2026-03-26-impersonate-panel-design.md b/docs/superpowers/specs/2026-03-26-impersonate-panel-design.md new file mode 100644 index 0000000..0e1a724 --- /dev/null +++ b/docs/superpowers/specs/2026-03-26-impersonate-panel-design.md @@ -0,0 +1,198 @@ +# Impersonate Panel — Design Spec + +A Discohook-style webhook message editor inside the Aurora admin panel for sending messages as custom characters, with reusable presets. + +## Summary of Decisions + +| Decision | Choice | +|----------|--------| +| Channel targeting | Pick channel each time (dropdown) | +| Preset storage | PostgreSQL (new table) | +| Editor layout | Side-by-side (builder left, preview right) | +| Component adding | Drag & drop from palette | +| Preset management | Separate "Presets" tab with card grid | +| JSON editing | Bidirectional visual ↔ JSON toggle | +| Format support | Classic (content + embeds) AND Components V2 | + +## Data Model + +### New table: `webhook_presets` + +| Column | Type | Description | +|--------|------|-------------| +| `id` | serial PK | Auto-increment ID | +| `name` | varchar(100) | Preset display name | +| `username` | varchar(80) | Webhook display name | +| `avatar_url` | text, nullable | Avatar image URL | +| `payload` | jsonb | Full webhook payload (content, embeds, components) | +| `created_by` | bigint | Discord user ID of creator | +| `created_at` | timestamp | Creation time | +| `updated_at` | timestamp | Last modified | + +The `payload` column stores the complete webhook JSON body and is the source of truth. The visual editor reads/writes this JSONB directly. + +**Notes:** +- `created_by` uses `bigint('created_by', { mode: 'bigint' })` with a foreign key reference to `users.id` (`onDelete: CASCADE`) +- `created_at` uses `.defaultNow()` +- `updated_at` is set by the application on every write (no database trigger) +- No `guild_id` scoping — Aurora is a single-guild bot +- The new schema file must be re-exported from `shared/db/schema/index.ts` +- Backend validation: reject payloads larger than 100KB + +## API Endpoints + +All endpoints are protected behind existing admin auth. + +### Presets CRUD + +- `GET /api/impersonate/presets` — list all presets +- `POST /api/impersonate/presets` — create preset +- `PUT /api/impersonate/presets/:id` — update preset +- `DELETE /api/impersonate/presets/:id` — delete preset + +### Sending + +- `POST /api/impersonate/send` — send webhook message to a channel + - Body: `{ channelId: string, username: string, avatarUrl?: string, payload: object }` + - **Bridge pattern:** The route handler imports `BotClient` to resolve the channel by ID (`client.channels.fetch(channelId)`) and obtain the client user. These discord.js objects are passed to the existing `sendWebhookMessage` utility from `bot/lib/webhookUtils.ts`. This is acceptable because `api/` already runs in the same Bun process as the bot. + +### Channels + +- `GET /api/impersonate/channels` — fetch guild text channels for the channel picker + - Returns `{ id, name, parentName }` grouped by category + +## Frontend Architecture + +### Page Structure + +Two tabs at the top of the Impersonate page: **Editor** and **Presets**. + +### Editor Tab (Side-by-Side) + +**Left pane — Builder:** + +- **Top bar:** Username input, avatar URL input, channel dropdown, format toggle (Classic / Components V2), JSON/Visual toggle +- **Component palette:** Draggable component types. Components V2: Text Display, Section, Media Gallery, Separator, Container, File, Action Row. Classic: Content, Embed +- **Message canvas:** Drop zone where components are arranged. Each dropped component expands into an inline collapsible form editor. Drag to reorder via `@dnd-kit/core` +- **Bottom bar:** "Send" button and "Save as Preset" button + +**Right pane — Preview:** + +- Discord-styled message preview (dark theme, `#313338` background) +- Avatar circle + username + timestamp header +- Live-renders the current payload on every change +- Visual approximation of Discord's rendering, not pixel-perfect + +### JSON Mode + +Toggling to JSON replaces the visual builder with a monospace code editor. Edits sync bidirectionally. Invalid JSON shows an inline error and blocks switching back to visual mode until fixed. + +### Presets Tab + +- Card grid of saved presets showing avatar, name, and truncated payload preview +- Click a card to load it into the editor tab +- Edit/delete actions on each card + +### File Structure + +``` +panel/src/ +├── pages/ +│ ├── Impersonate.tsx # Main page, tab switching, top-level state +│ └── impersonate/ +│ ├── Editor.tsx # Side-by-side builder + preview layout +│ ├── Preview.tsx # Discord-style message renderer +│ ├── Presets.tsx # Preset card grid +│ ├── ComponentPalette.tsx # Draggable component type list +│ └── components/ +│ ├── TextDisplayEditor.tsx +│ ├── SectionEditor.tsx +│ ├── MediaGalleryEditor.tsx +│ ├── SeparatorEditor.tsx +│ ├── ContainerEditor.tsx +│ ├── FileEditor.tsx +│ ├── ActionRowEditor.tsx +│ ├── EmbedEditor.tsx # Classic mode +│ └── ContentEditor.tsx # Classic mode +├── lib/ +│ └── useImpersonate.ts # API hook for presets CRUD + send + channels +``` + +Backend: +``` +shared/db/schema/ +│ └── webhook-presets.ts # New schema file (re-export from index.ts) + +shared/modules/impersonate/ +│ └── impersonate.service.ts # Preset CRUD + send logic + +api/src/routes/ +│ └── impersonate.routes.ts # Route handler (register in index.ts protectedRoutes) +``` + +### Panel Wiring + +- Add `"impersonate"` to the `Page` union type in `Layout.tsx` +- Add nav item to `navItems` array in `Layout.tsx` with appropriate Lucide icon +- Add conditional render branch in `App.tsx` + +**Note:** The `pages/impersonate/` sub-directory is a new pattern — existing pages are flat files. This is justified by the complexity of this feature (9+ component files). Flat pages remain appropriate for simpler pages. + +## Component Editors + +Each component type gets an inline collapsible editor card on the canvas. + +### Components V2 + +| Component | Editable Fields | +|-----------|----------------| +| **Text Display** | Markdown content textarea | +| **Section** | Text content, accessory type (button or thumbnail), accessory config (URL, label, style) | +| **Media Gallery** | List of media items: URL, alt text, spoiler toggle. Add/remove items | +| **Separator** | Spacing size toggle (small/large) | +| **Container** | Accent color picker, nested drop zone (accepts Text Display, Section, Media Gallery, Separator, Action Row, File) | +| **File** | URL input, filename | +| **Action Row** | Buttons: label, style (Primary/Secondary/Success/Danger/Link), URL/custom ID, emoji, disabled toggle. Select menus: placeholder, options list, min/max values | + +### Classic Mode + +| Component | Editable Fields | +|-----------|----------------| +| **Content** | Markdown textarea | +| **Embed** | Title, description, URL, color picker, timestamp, author (name, icon URL), footer (text, icon URL), image URL, thumbnail URL, fields (array of name, value, inline toggle) | + +### Webhook-Level Options + +- `tts` toggle +- `thread_name` input (for forum channels) +- `flags` (suppress embeds/notifications) +- When Components V2 format is selected, the payload must include `flags: 32768` (`IS_COMPONENTS_V2` flag, `1 << 15`). This is set automatically by the editor when the format toggle is on Components V2. + +## Preview Renderer + +Renders a Discord-style message mock in the right pane: + +- Dark background (`#313338`) +- Avatar circle + username + "Today at HH:MM" timestamp header +- Components V2: containers with accent-colored left border, text blocks with markdown rendering, media gallery as responsive image grid, buttons as pill-shaped elements with Discord color scheme, separators as horizontal rules +- Classic: content as rendered markdown, embeds with colored left border, field grids, inline images +- Live updates on every change + +This is a visual approximation for authoring purposes, not a 1:1 Discord replica. + +## Error Handling + +| Scenario | Behavior | +|----------|----------| +| Invalid JSON on toggle | Show inline error, block switch to visual until fixed | +| Send failure | Display Discord API error message inline (e.g., "Missing permissions") | +| Empty payload | Disable Send button | +| Discord payload limits | Validate against limits (6000 char embeds, 10 components per action row, 5 action rows) and show warnings | +| Channel permission errors | Surface "Bot lacks MANAGE_WEBHOOKS permission" clearly | +| Invalid avatar URL | Lightweight `https://` check; Discord rejects bad URLs on send | +| Preset name collision | Allowed — presets identified by ID | + +## Dependencies + +- `@dnd-kit/core` + `@dnd-kit/sortable` — drag and drop +- No other new dependencies expected; existing stack (React, Tailwind, Lucide) covers the rest