Web-based config editor for the Azalea Discord moderation bot.
This is a sibling project — it lives next to the bot's working tree, reads and writes azalea/configs/<guild_id>.yml, and triggers the bot to reload via pm2.
- Backend: Bun + Hono, single PM2 instance. SQLite (
bun:sqlite) for sessions and the audit log. - Frontend: React 18 + Vite + Tailwind 4 + Monaco editor + react-hook-form. Source in
ui/, builds toui/dist/. - Auth: Discord OAuth (
identify+guilds); per-guild permission re-derivation per request mirrors the bot'sGuildConfig.hasPermission. - Save pipeline: lock → validate → mtime-check → backup → atomic write →
pm2 reload→ poll the bot's/healthz→ two-step rollback on failure.
In dev, Vite serves the UI on EDITOR_PORT (default 7476) and proxies /api, /auth, /healthz to Hono on BACKEND_PORT (default 7477). In prod, only Hono runs — it serves both the API and the built ui/dist.
cp .env.example .env
# fill in DISCORD_CLIENT_ID / SECRET / REDIRECT_URI / TOKEN / SESSION_SECRET / AZALEA_REPO_PATH
bun install
# Dev — Vite + HMR on EDITOR_PORT, Hono on BACKEND_PORT, proxied together.
op run --env-file=.env -- bun run dev
# Or if .env contains literal secrets:
bun run dev
# Production — single Hono process serves API + built UI on EDITOR_PORT.
bun run build
op run --env-file=.env -- bun startThe bot must be running with the /healthz endpoint available (Bun.serve on 127.0.0.1:7475 by default — see azalea/src/utils/health.ts).
Open http://127.0.0.1:7476 once the editor is up. Sign in with Discord, pick a guild, edit the YAML or use the structured Permissions form, save.
A fresh guild config has no role granting manage_guild_config, so the editor will refuse access until one is configured. Set BOOTSTRAP_USER_IDS to your Discord user ID, log in, grant manage_guild_config to a staff role, then unset the env var.
| Script | Description |
|---|---|
bun run dev |
Start the server in watch mode. |
bun start |
Start the server (production). |
bun run typecheck |
tsc --noEmit. |
bun test |
Run unit tests. |
| Method | Path | Notes |
|---|---|---|
GET |
/auth/login |
302 to Discord OAuth |
GET |
/auth/callback |
Code exchange, sets session |
POST |
/auth/logout |
Destroys session |
GET |
/api/me |
Identity + manageable guilds |
GET |
/api/health |
Proxies the bot's /healthz for the version-drift banner |
GET |
/api/guilds/:guildId/config |
Current YAML + parse + mtime |
POST |
/api/guilds/:guildId/config/validate |
Server-side safeParse |
POST |
/api/guilds/:guildId/config |
Save pipeline (validate, write, reload, verify, rollback) |
GET |
/api/guilds/:guildId/config/backups |
List backups |
POST |
/api/guilds/:guildId/config/restore |
Restore a backup |
GET |
/api/guilds/:guildId/discord/{channels,roles,emojis} |
Bot-token-backed proxy with 30s cache |
GET |
/api/guilds/:guildId/audit |
Audit log |
- Never imports
GuildConfigorConfigManagerfrom the bot — both callprocess.exit(1)on validation failure. Always usesrawGuildConfigSchema.safeParsedirectly. - Always triggers a full
pm2 reload. In-place hot-swap viaaddGuildConfigwould leak cron jobs (scheduled messages, review reminders, report removal). - Bind
127.0.0.1only; front with a reverse proxy that terminates TLS.