5.9 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Scope
This is the settings-site subdirectory of the broccolini-bot repo. It is a separate Express process that provides an admin web UI for editing the bot's runtime config. It is not part of the bot's Node process.
The parent repo's rules in /opt/broccolini-bot/CLAUDE.md still apply here — especially CommonJS only, read before write, and no unsolicited refactors. Read that file alongside this one.
Commands
npm start— run the settings site (node server.js).npm run dev— run withnode --watchfor auto-reload.- No lint, no test framework, no build step. Frontend is vanilla JS served from
public/— no bundler. - Deploy via its own compose file:
docker compose up --build -dfrom this directory. Container namebroccolini-settings, joins externalbroccoli-net.
Architecture
Two processes, one .env
The settings site is a thin HTTPS-oriented proxy in front of the bot's internal API:
browser ──► settings server.js (:SETTINGS_PORT, default 12752)
│ session auth (SETTINGS_ADMIN_PASSWORD)
▼
bot internalApp (broccoli-net only, INTERNAL_API_PORT, default 12753)
│ header auth (x-internal-secret = INTERNAL_API_SECRET)
▼
routes/internalApi.js in /opt/broccolini-bot
server.js loads ../.env (the bot's env file) — both processes share it. docker-compose.yml also mounts env_file: ../.env, not a local one. There is no settings-site-specific env beyond what's in .env.example.
Proxied endpoints
server.js exposes five authenticated endpoints that forward to the bot's /internal/* API via callBot():
| Settings route | Bot route |
|---|---|
GET /api/config |
GET /internal/config |
POST /api/config |
POST /internal/config |
GET /api/discord/guild |
GET /internal/discord/guild |
POST /api/restart |
POST /internal/restart |
GET /api/restart/status |
GET /internal/restart/status |
GET /api/notifications/alerts |
GET /internal/notifications/alerts |
GET /api/notifications/state |
GET /internal/notifications/state |
POST /api/notifications/toggle |
POST /internal/notifications/toggle |
Every response-shape change in the bot's /internal/* handlers (routes/internalApi.js) is a breaking change here. The bot also gates POST /internal/config on an ALLOWED_CONFIG_KEYS allowlist — adding a new field to the UI requires adding the key to that Set in the bot first, otherwise the save returns 400 for that key.
Second admin password
server.js accepts an optional SETTINGS_ADMIN_PASSWORD_2. If set, the /login handler grants the same session for either password; no audit distinction between them. The primary SETTINGS_ADMIN_PASSWORD is still required at startup — only the second is optional. Both are redacted by the bot's GET /internal/config response.
Session cookie requires HTTPS
server.js:20-26 sets cookie.secure: true. Browsers will refuse to persist the session cookie over plain HTTP, so login silently fails when not behind an HTTPS reverse proxy (SETTINGS_DOMAIN is the deployed domain). If you're reproducing a login bug, check this first before debugging auth logic. The session secret falls back to 'fallback-secret-change-me' when INTERNAL_API_SECRET is unset — don't rely on the fallback in any environment that matters.
Client-side routing
public/js/ is split into focused modules (phase 4 refactor): app.js (bootstrap), router.js, fields.js, notifications.js, discord.js, login.js, util.js — no bundler, loaded via <script> tags. Routes live in the ROUTES map (router.js:4); the server has a catch-all back to index.html (server.js:202, Express 5 '/*splat' syntax), so adding a client route only requires editing ROUTES.
Config field binding (frontend)
Any form element with data-key="SOME_CONFIG_KEY" participates in the editor:
populateFields()(fields.js:11) fills it fromGET /api/configand wires change listeners.- Checkboxes serialize to the strings
'true'/'false', and<input type="color">serializes to0xRRGGBB— this matches how the bot stores these values. pendingChangesaccumulates diffs;saveConfig()POSTs the whole diff at once.data-smart="channel|category|role|member|multi-member"swaps the bare<input>for a searchable Discord picker backed byGET /api/discord/guild(seepublic/js/discord.js).
To add a new editable config field: (1) add the key to the bot's ALLOWED_CONFIG_KEYS, (2) add a <input data-key="NEW_KEY"> (optionally data-smart=…) inside the appropriate .section in public/index.html. No JS changes needed.
Notification thresholds editor
The Notifications section is not a simple data-key field — it's a custom editor in notifications.js that serializes into a single hidden NOTIFICATION_THRESHOLDS_JSON field. Alert metadata is now a dynamic registry (phase 5): the bot is canonical and serves it via GET /api/notifications/alerts; notifications.js uses FALLBACK_TAB_KEYS only if the fetch fails. To add a new alert key, register it in the bot (not in this codebase) — the UI picks it up automatically on next load. Threshold values accept whole numbers or duration strings matching ^(\d+[mhd])+$ (e.g. 15m, 1h, 1d6h).
Gotchas
- The frontend has no framework and no build — edit
public/js/*.jsdirectly; changes are live on reload. getaddrinfofailures fromcallBot()surface to the UI as "Bot unreachable" (502). This is almost always the bot process being down or the internal port being wrong, not a bug in this codebase.docker-compose.ymlbinds the port to the Tailscale IP100.114.205.53:12752— not0.0.0.0. Changing that binding has security implications.