Files
aurorabot/docs/superpowers/specs/2026-03-26-impersonate-panel-design.md
syntaxbullet 0f871026eb docs: add impersonate panel design spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:08:23 +01:00

8.7 KiB

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