untrack CLAUDE.md

This commit is contained in:
2026-04-21 17:19:39 +00:00
parent 5de05a0d01
commit b764bc98c7

129
CLAUDE.md
View File

@@ -1,129 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Default mode: reviewer-first
Default output is **scoped improvement prompts**, not code edits. Output format:
```
## [Short title]
**Files:** [files to read/modify]
**Problem:** [1-2 sentences]
**Fix:** [specific instructions — what to change, not how to think about it]
**Verify:** [how to confirm]
```
Keep each prompt to a 520 minute task; decompose larger issues.
When the user asks for direct fixes, make them — but still avoid unsolicited refactors, rename sweeps, or cleanup beyond the stated scope.
## Project
- **broccolini-bot** — Discord ticketing + support bot for Indifferent Broccoli (game hosting).
- **Repo:** `/opt/broccolini-bot/` · **Gitea:** `ssh://git@100.114.205.53:2222/indifferentketchup/broccolini-bot.git`
- **DB:** Self-hosted MongoDB on same host as bot, database `broccoli_db`. Dedicated user per DB.
- **Host port 8892 → container port 5000** (`CONFIG.PORT`, env `DISCORD_ONLY_PORT`).
- **Deploy:** `cd /opt/broccolini-bot && git pull && docker compose up --build -d` · tail: `docker logs broccolini-bot --tail 50 -f`
## Commands
- `npm start` — run the bot (entry: `broccolini-discord.js`).
- `npm run start:test` — run with `ENV_FILE=.env.test`.
- `npm run start:1p` / `start:test:1p` — inject secrets via 1Password CLI (`op run`).
- `npm run test-mongodb` / `test-mongodb:test` — connectivity probe; no test suite exists.
- No lint step configured. No unit/integration test framework.
- **Verification:** prefer `node --check <file>` for syntax, and inline `node -e "..."` for behavior. For tightly-coupled modules, stub deps via `Module._resolveFilename` override (see `services/channelQueue.js` tests in session history).
Many files under `scripts/` are one-shot maintenance utilities (backups, user lookups, transcript mapping). They are not wired into CI or into the bot's runtime.
## Stack
Node.js **CommonJS** · Discord.js 14 · Express 5 · Mongoose 6 · googleapis · express-rate-limit · p-queue · dotenv/dotenv-expand.
## Hard Rules
1. **CommonJS only.** `require` / `module.exports`. Never `import`.
2. **Read before write.** Never propose or make changes to a file without first reading its current contents.
3. **Route channel operations through `services/channelQueue.js`**: `enqueueSend`, `enqueueRename`, `enqueueMove`, `enqueueDelete` (awaits both rename+send tails before deleting). Bypass sites are tagged `// TODO(queue-migrate):` — grep to find them; migrate incrementally when touching.
4. **Logging is fire-and-forget.** Never `await logSystem/logError/logAutomation/logGmail/...`. Chain `.catch(() => {})` instead.
5. **Use `ChannelType` enum from `discord.js`**, not bare integers (`0`, `4`, `5`, `12`, `15`).
6. **Mongoose schema defaults:** pass function references (`default: Date.now`), never invocations (`default: Date.now()` pins all documents to module-load time).
7. **No unsolicited refactors.** Don't rename, reorganize, or restructure beyond the fix's scope.
8. **Backup before destructive data ops.** Provide the backup command first when the fix touches collections/files.
## Architecture
Single Node process. Entry: `broccolini-discord.js`.
### Startup order
1. Module load: env validation, Discord `Client` created, `interactionCreate` / `messageCreate` listeners registered, `client.login(...)` called.
2. Public Express app (`app`) is defined at module scope with a **503 gate** — any `/api/*` request before `appReady` returns 503.
3. `client.once('ready')` (fires after Discord handshake): connects MongoDB, mounts bOSScord routes on `/api` (only if `BOSSCORD_API_KEY` set), calls `app.listen(CONFIG.PORT, CONFIG.HEALTHCHECK_HOST)`, sets `appReady = true`, then starts all background `setInterval`s.
4. The **internal** Express app (`internalApp`) listens separately on `0.0.0.0:INTERNAL_API_PORT` inside the bot container at module load, guarded by `INTERNAL_API_SECRET`. Not publicly exposed — reachable only from peers on the `broccoli-net` docker network (notably the settings-site container).
### Two HTTP surfaces
- **Public (`app`)** — `GET /` healthcheck + `/api/*` (bOSScord consumer). CORS origin is `process.env.BOSSCORD_CLIENT_ORIGIN` (default `http://100.114.205.53:3081`). Rate-limited 60 req/min/IP. Auth: `Authorization: Bearer ${BOSSCORD_API_KEY}`.
- **Internal (`internalApp`)** — `broccoli-net` only (binds `0.0.0.0` inside the bot container; no host `ports:` publish), `/internal/*`. Rate-limited 10 req/min. Auth: `x-internal-secret` header. `POST /config` enforces an explicit `ALLOWED_CONFIG_KEYS` allowlist; unknown keys return 400. `POST /restart` exits the process so the container supervisor restarts it.
`routes/internalApi.js` is required at module scope by `broccolini-discord.js` *before* the parent's `module.exports` populates — reaching back to the parent (e.g., `trackInterval`, `trackTimeout`, `clearGmailPollInterval`) must use a **lazy `require('../broccolini-discord')` inside the handler**, not a top-level destructure.
### Intervals & shutdown
- Every `setInterval` inside `ready` is wrapped via `trackInterval(...)` into the module-scoped `activeIntervals` Set.
- `handleShutdown(signal)` is idempotent (`shuttingDown` flag): clears every tracked interval, closes both HTTP servers, calls `client.destroy()`, calls `closeMongoDB()`, then `process.exit(0)`. Wired to SIGTERM/SIGINT.
- `setGmailPollInterval(ms)` and `clearGmailPollInterval()` manage the Gmail poll handle and keep it in sync with `activeIntervals`.
### Interaction error handling
Every `interactionCreate` branch runs through `runHandler(name, interaction, fn)` which catches, `logError`s, and replies ephemerally `'Something went wrong.'` (uses `followUp` when the interaction is already deferred/replied). Setup buttons have their own try/catch for a custom error message.
### Tickets (`services/tickets.js`, `models.js`)
- `Ticket` schema has indexes on `{gmailThreadId}` (unique), `{status, lastActivity}`, `{senderEmail, status}`, `{discordThreadId}`.
- **Discord-originated tickets** use `gmailThreadId` with prefix `discord-` / `discord-msg-` — skip the Gmail reply path entirely.
- Renames route through `utils/renamer.js` (RENAMER_BOT secondary token). On 401/403/429 from the secondary, `services/channelQueue.js` falls back to the primary bot via `channel.setName`. `canRename()` in `services/tickets.js` is retained as an always-ok shim for back-compat. `Ticket.renameCount` / `Ticket.renameWindowStart` remain in the schema but are now unread/unwritten orphan fields.
- `getOrCreateTicketCategory()` handles Discord's 50-channels-per-category ceiling by creating `"<name> (Overflow N)"` categories; `cleanupEmptyOverflowCategory()` removes empties. The primary category is never deleted.
- Scheduled jobs in `ready`: `checkAutoClose`, `checkAutoUnclaim`, `reconcileDeletedTicketChannels`, plus `services/staffNotifications.js#notifyAllStaffUnclaimed` and the pattern/surge/chat checkers.
### Gmail bridge (`gmail-poll.js`, `services/gmail.js`)
- Polls `is:unread category:primary`, creates or appends to ticket channels.
- **Auth failure halts polling.** On `invalid_grant` / `unauthorized` / 401: `pollSuspended = true`, the poll interval is cleared via `require('./broccolini-discord').clearGmailPollInterval()`, admin is DM'd once. Polling does **not** auto-retry — container must restart after re-auth.
- `services/gmail.js` exports `sendGmailReply`, `sendTicketClosedEmail`, `sendTicketNotificationEmail`, `getGmailClient`. All HTML bodies go through `escapeHtml()`; `Date.now`-derived variables in templates come from `CONFIG` (`TICKET_CLOSE_MESSAGE`, `TICKET_CLOSE_SIGNATURE`, `SUPPORT_NAME`, `LOGO_URL`, `SIGNATURE`, `EMAIL_SIGNATURE`).
### Pattern / surge / chat (`services/patternStore.js` et al.)
- In-memory counters bucketed into `today` / `week` / `month`, with scheduled resets at midnight / Monday 00:00 / 1st 00:00.
- `escalatingCooldowns` entries carry a `lastUsed` timestamp; a 6-hour interval prunes entries idle for >48h. The cleanup interval is `.unref()`-ed so shutdown isn't blocked by it.
### `.env` persistence (`services/configPersistence.js`)
- Values are stored in **backtick** containers because dotenv v17 only decodes `\n`/`\r` inside `"…"` (not `\"` or `\\`) — backticks preserve quotes + literal newlines verbatim. `readEnvFile` joins multi-line backtick values; `writeEnvFile` re-reads after write and throws on key-count mismatch.
## bOSScord integration
bOSScord is a separate React + Express cockpit app that consumes this bot's `/api/*` endpoints.
- Base URL: `http://100.114.205.53:8892/api` · Bearer `${BOSSCORD_API_KEY}`.
- bOSScord uses its own database (`bosscord_db`) — do not mix models.
- **Response-shape changes on `/api/*` are breaking** for bOSScord. Coordinate or version.
## Known bad state
- **Gmail `invalid_grant`** — `REFRESH_TOKEN` is a stale placeholder. Poll suspends automatically on auth error; the rest of the bot still works. Fix by regenerating the token (`node get-refresh-token.js`) and restarting.
- **`STAFF_EMOJIS` encoding** — some emoji entries render malformed. Root cause not identified.
- **Escalation button** — handler misfires in some flows. Root cause not identified.
Do not re-report these as new findings.
## Environment highlights
Names and full tables are in `README.md` / `.env.example`. Ones that commonly trip up new code:
| Var | Notes |
|-----|-------|
| `DISCORD_TOKEN` **or** `DISCORD_BOT_TOKEN` | First non-empty after trim wins. |
| `DISCORD_ONLY_PORT` | Maps to `CONFIG.PORT` (default 5000). |
| `HEALTHCHECK_HOST` | Omit for all-interfaces; set `127.0.0.1` for local-only. |
| `BOSSCORD_API_KEY` | Without it, `/api/*` is never mounted. |
| `BOSSCORD_CLIENT_ORIGIN` | CORS origin for bOSScord (not `BOSSCORD_CORS_ORIGIN`). |
| `INTERNAL_API_SECRET` | Without it, the internal settings API is never started. |
| `INTERNAL_API_PORT` | Internal app's port (127.0.0.1 bind). |
| `REFRESH_TOKEN` | Gmail OAuth; currently stale — see Known bad state. |
## Settings site
`settings-site/` contains a separate Express app (`settings-site/server.js`) for the admin UI — it talks to `internalApp` via `INTERNAL_API_SECRET`. It is **not** part of this bot's process. Changes to the bot's `/internal/config` contract (e.g., the `ALLOWED_CONFIG_KEYS` set) may break the settings UI. See `settings-site/CLAUDE.md` for that subproject's architecture and conventions.
Its Docker build context is `settings-site/` only — parent-repo files (e.g., `utils.js`) are unreachable inside the container. Any shared helper must be inlined or the build context widened in `docker-compose.yml`.