# 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
