5.5 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 (127.0.0.1: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.
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.