Mongoose 6 entered maintenance/EOL in 2023; current major is 8.x. No source
changes required — every API the codebase uses is identical between v6 and
v8:
- models.js schema DSL (Schema, default: Date.now function refs, enum,
unique, required, index) is unchanged.
- db-connection.js connect options (serverSelectionTimeoutMS,
socketTimeoutMS) and connection event names (error/disconnected/
reconnected) are unchanged.
- All queries already use the v7+-required APIs: countDocuments (not the
removed count()), updateOne/findOneAndUpdate (not the removed update()),
bulkWrite, .lean() — no callback-based queries, no Model.remove(),
no findOneAndRemove.
- findOneAndUpdate sites all explicitly pass { new: true } so the v7
default-flip from old-doc to new-doc doesn't change behavior.
- strictQuery default flipped to false in v7+; codebase only filters on
schema-defined fields, so the change is a no-op.
Verification on this commit:
- node -e "require('./db-connection')" loads cleanly; modelNames() returns
the expected six models.
- Every source file under handlers/, services/, routes/, gmail-poll.js,
broccolini-discord.js requires cleanly.
- new Ticket(...).validateSync() accepts valid docs, rejects invalid enum
values, and Date defaults still fire.
- vitest run still passes (87/87) — pure-function suite is unaffected by
the upgrade but confirms no regression in the dependency-shared modules.
Production verification (live DB CRUD: create/find/updateOne/deleteOne/
bulkWrite) still owed via docker compose up --build -d on the homelab.
The 1028-line handlers/commands.js bundled escalation logic + force-close
flow + /response tag CRUD + /panel + /signature + context-menu handlers +
several config-toggle slash commands. After the dispatch-table refactor it
was still a god module. Split into handlers/commands/ with one file per
topic; require('./commands') resolves to handlers/commands/index.js
(handlers/commands.js is removed).
Layout:
helpers.js — requireStaffRole, fetchLoggingChannel
(cross-submodule, kept here to avoid cycles with index.js)
escalation.js — runEscalation, runDeescalation, handleEscalate, handleDeescalate
(run* are still exported via index.js for handlers/buttons.js)
close.js — handleForceClose, handleCancelClose, handleCloseTimer
+ finalizeForceClose / postTranscript (timer callback)
response.js — handleResponse + send/create/edit/delete/list subcommands
+ handleAutocomplete (only /response autocompletes)
panel.js — handlePanel, buildPanelButtonRow, handleSignature
contextMenu.js — handleCreateTicketFromMessage, handleViewUserTickets
index.js — dispatch tables, handleCommand/handleContextMenu, plus the
short-and-not-thematic handlers (notifydm, add, remove,
transfer, move, topic, staffthread, pinmessages, gmailpoll,
help) and the public re-exports.
No behavior change — every imported name, every Discord call, every DB
write, every embed, every reply payload preserved verbatim. Public surface
of require('./commands') is still { handleCommand, handleContextMenu,
handleAutocomplete, runEscalation, runDeescalation }.
Largest single module is now index.js at 299 lines; others are 33–214.
Split the original 309-line poll() into single-responsibility helpers and a
thin orchestrator. No behavior change — every Gmail API call, Discord call,
DB write, and log line stays in the same order with the same arguments.
Helpers extracted (module-private):
- locateGuild(client) — DISCORD_GUILD_ID lookup with fallback warning.
- parseGmailMessage(email) — header parsing, body decode, dual cleanup
(firstBody for new-ticket message, followupBody for thread append).
- findOrCreateTicketChannel(guild, parsed, number) — category resolution
+ channel.create with the existing two-stage error handling.
- linkPreviousTranscripts(ticketChan, threadId, client) — best-effort
prior-transcript link on reopen.
- markGmailMessageRead(gmail, msgRef) — wraps the batchModify call used
in five places across the original.
- oauthSuspendIfPermanent(err, client) — invalid_grant/invalid_client
classify, suspend polling, clear interval, DM admin once. Returns bool.
poll() is now the orchestrator: list → locate guild → for each message,
parse → look up existing ticket → branch (append-followup vs new-ticket
flow) → mark read. The new-ticket branch stays inline in poll() per the
"keep poll() as orchestration" intent.
QUAL-006 store ticket.creatorId on creation; legacy split-pop returned the
message ID for discord-msg-* tickets, breaking transcript DM, close
log, and channel rename for context-menu-created tickets. Adds the
field to the Ticket schema and writes a one-shot backfill script
(scripts/backfill-creatorId.js, dry-run by default).
QUEUE-001 add enqueueOverwrite + enqueueTopic to services/channelQueue.js
(chain on renameChains alongside enqueueMove). Migrate handleAdd /
handleRemove / handleMove / handleTopic so permissionOverwrites,
setParent, and setTopic no longer race pending renames or sends.
handleMove now uses the existing enqueueMove. Initial overwrites in
handleTicketModal stay inline; channel doesn't exist yet so no race.
DISCORD-001 replace ephemeral: true with flags: MessageFlags.Ephemeral across
broccolini-discord.js, handlers/sharedHelpers.js, handlers/buttons.js,
handlers/commands.js. runDeferred opts now take { flags } directly.
SEC-003 /gmailpoll min interval is 30s. Drop the 5s/10s slash-command
choices and clamp Math.max(30000, ms) in handleGmailPoll for
defense in depth.
QUAL-001 upgrade silent .catch(() => {}) on the lastActivity updateOne in
handlers/messages.js to log via logError, so transient Mongo errors
surface in the debug channel instead of disappearing.
QUAL-002 drop await from logError/logWarn calls in services/staffThread.js
and services/pinMessage.js — fire-and-forget per CLAUDE.md hard rule.
QUAL-003 wrap stray setTimeouts (handleConfirmCloseRequest force-close timer,
runFinalClose channel-delete + overflow-cleanup, checkAutoClose
delete-after-email) in trackTimeout via lazy require so they clear
on shutdown.
Each customId now maps to a named handler in one of two tables:
FREE_BUTTON_HANDLERS (open-ticket panel, tag-delete cancel — no ticket
lookup) or TICKET_BUTTON_HANDLERS (anything fired inside a ticket channel
— the dispatcher does the lookup once before delegating). The dynamic
`confirm_delete_tag::*` id is matched by prefix.
To find a button's logic, search handle<Name>Button or handleTagDelete*.
Other cleanups in the same pass:
- Move findTicketForChannel and runDeferred from handlers/commands.js to
the new handlers/sharedHelpers.js so both files share one source of
truth. runDeferred now also calls logError(verb, ...) — was logged ad
hoc in buttons.js, missing in commands.js. Strictly additive.
- Hoist three inline `require('../services/...')` calls (staffThread,
pinMessage, debugLog) to top imports.
- Collapse escalate_to_tier2 and escalate_to_tier3 into one
handleEscalateButton(interaction, ticket) that derives the tier from
customId. Same for confirm_close / confirm_close_with_email /
confirm_close_no_email — one handleConfirmCloseRequest deriving
sendEmail from customId.
- Decompose the 156-line handleConfirmClose into runFinalClose +
buildTranscriptText + formatDateForTranscript + renderTranscriptHeader
+ dmTranscriptToCreator + postCloseLogEntry. Each piece is testable in
isolation.
- Decompose handleClaim into applyClaim + applyUnclaim.
- Extract buildOpenTicketModal() and postTicketWelcomeEmbeds() so the
ticket-creation modal flow is readable top-to-bottom.
No behavior change. handleButton + handleTicketModal exports preserved;
24/24 modules load clean (sharedHelpers.js is the new one).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each slash command and context-menu entry is now its own named function;
handleCommand looks the name up in COMMAND_HANDLERS and delegates. Finding
where a command lives is now "grep handle<Name>" instead of scrolling 600
lines of sequential ifs.
Other cleanups in the same pass:
- Restore ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle
to the top-level discord.js destructure. ActionRowBuilder was used at
runtime by /response delete and /panel but had been dropped from the
imports based on a stale "unused" diagnostic — those paths would have
thrown ReferenceError. Hoisting the previously-inline /signature
imports as well.
- Hoist `logTicketEvent` to the top imports (was inline-required at three
callsites).
- Extract findTicketForChannel(), runDeferred(), and fetchLoggingChannel()
helpers — replaces the lookup-then-defer-then-try-catch boilerplate
repeated in nearly every branch.
- Pull the force-close timer body into finalizeForceClose() and
postTranscript() so the timer registration is one line.
- Pull the panel button-row construction into buildPanelButtonRow().
- Split /response into its own RESPONSE_SUBCOMMANDS dispatch + per-sub
handlers.
- Consolidate the duplicated transcript date formatter into one local fmt().
No behavior change. All 23 modules still load clean; handleCommand,
handleContextMenu, handleAutocomplete, runEscalation, and runDeescalation
exports are preserved (handlers/buttons.js imports the last two).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rename CONFIG.TRANSCRIPT_CHAN -> CONFIG.TRANSCRIPT_CHANNEL_ID and
CONFIG.LOG_CHAN -> CONFIG.LOGGING_CHANNEL_ID across 9 callsites so
CONFIG keys match their .env names — no more "grep .env, find nothing"
for new readers
- Replace handlers/commands.js#hasStaffRole with utils.js#isStaff
(was a verbatim copy)
- Delete utils.js#enforceEmbedLimit and its 2 callsites; both inputs are
bounded well under the 6000-char Discord embed cap, so the trim was
defensive code that never fired
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove no-op log stubs (logGmail, logAutomation, logSecurity, logSystem)
and ~17 callsites; dead counters in tickets.js and gmail-poll.js go too
- Dedup three near-identical Gmail send paths into sendThreadedEmail helper
- Drop dead Mongoose fields: broccoliniTicketId, lastSyncedBroccoliniArticleId,
renameCount, renameWindowStart, reminderSent, staffChannelId,
unclaimedRemindersSent, lastMessageAuthorIsStaff
- Drop dead config fields and their .env.example entries
- Inline api/botClient.js (3-line wrapper, 2 callers)
- Trim unused exports across utils.js, tickets.js, configSchema.js, debugLog.js
- Fix handlers/messages.js to use isStaff() — old partial check ignored
ADDITIONAL_STAFF_ROLES, so those members were treated as customers
- Drop unused deps p-queue + dotenv-expand; move mongodb to devDependencies
Net: -583 LOC source + -57 LOC lockfile. All 23 modules load clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 untracked .bak*/.dubignore files and empty docs/architecture, docs/setup, docs/ also removed from disk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gmail-poll.js was hand-rolling its own Close+Claim-only action row.
Replaced with getTicketActionRow({ escalationTier: 0 }) — same helper
panel-created tickets use. Email and Discord tickets now show identical
buttons on creation: Close, Claim, Escalate.
- secondary rename-bot token was set as RENAME_TOKEN in .env but utils/renamer.js reads RENAMER_BOT; silently no-op'd every rename (host .env renamed separately)
- services/tickets.js canRename gutted to an always-ok shim; Mongo 2/10min per-channel gate is redundant since renames flow through RENAMER_BOT's own bucket. Ticket.renameCount / renameWindowStart remain as orphan fields (no migration)
- handlers/buttons.js + commands.js: drop the four "Channel renamed too quickly" else-branches and the rename-countdown label suffix; replace .catch(() => {}) with .catch(err => logError('rename', err)...)
- services/channelQueue.js: executeRename falls back to channel.setName(currentName) when renamer throws err.fallback === true (401/403/429); classifies non-fallback errors as renameQueue:token/permission (401/403) or renameQueue:secondary-bot ratelimited (429)
- utils/renamer.js: on 401/403 throw err.fallback=true immediately; on 429 respect retry_after up to 2000ms then throw err.fallback=true
- docs: align CLAUDE.md, docs/api/DISCORD_API_VALIDATION.md, docs/architecture/CRITICAL_FILES_AND_HOW_IT_WORKS.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>