Commit Graph

48 Commits

Author SHA1 Message Date
cdb5db0082 Add per-staff metrics: StaffAction event log + /stats command
Event-sourced tracking of staff/ticket lifecycle actions, plus a /stats
command. Foundation for a future tickets-website analytics dashboard.

Data:
- StaffAction model (event log) + Ticket.game / Ticket.closedAt
- STATS_ADMIN_IDS config (who may view others' stats)

Recording (fire-and-forget, idempotent on real state transitions):
- claim, response (channel reply + /response send), escalate, de-escalate,
  transfer, close (4 sites), reopen — each denormalizes ticketType, tier,
  priority, game, requester (senderEmail / creatorId), guildId
- close events carry closerType / resolverId (claimer credit) / wasClaimed;
  transfer carries fromId / toId; reopen stamps resolverId
- conditional close transition helper (atomic open->closed + closedAt) shared
  by all four close paths

Query + command:
- pure period parser (presets + free-text) and stats shaper (per-metric keys)
- command-aware autocomplete dispatch
- /stats: period (autocomplete) + member (admin-gated) + source (all/email/
  discord), ManageMessages + staff-role gated, ephemeral, tier-labeled embed

288+ unit tests; timing/busiest-times data is collected but displayed later.
2026-06-05 02:02:48 +00:00
0fcffe8d33 Email replies: add spacing between signature and quoted message
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 22:44:25 +00:00
2ccdbf72aa Email ticketing fixes, comms polish, and .env cleanup
Inbound:
- Gmail poll query is:unread in:inbox (was category:primary, which matched
  nothing on a no-tabs Workspace inbox)

Outbound email:
- Closed/escalation auto-emails editable via TICKET_CLOSE_MESSAGE and new
  TICKET_ESCALATION_EMAIL_MESSAGE; drop the staff signature from closing emails
- Replies quote the customer's latest message (gmail_quote markup so clients
  collapse it), embed custom emoji inline via CID attachment, and strip Discord
  role mentions
- Tagline spacing fix in the company signature

Discord side:
- Suppress all mentions in log + transcript posts (no more pinging on close)
- Drop the staff-role ping from new-ticket and follow-up notifications
- Ticket channels inherit category permissions instead of setting per-channel
  overwrites (removes the Manage Roles requirement)

Gmail folders:
- Folder/label routing (gmailLabels.js) with /folder; close files to Complete

Config:
- Remove ~56 stale .env keys for long-removed features; refresh stale copy

Docs:
- Design specs for folder routing, email-flow toggle, and per-staff metrics
2026-06-04 22:05:20 +00:00
2fab3b97bf Remove dead/stale code, dedup close+escalation paths
Dead/stale removals (grep-confirmed no consumers):
- config: drop 9 unread CONFIG keys (ROLE_TO_PING_ID, SIGNATURE,
  REMINDER_*, RENAME_LOG_CHANNEL_ID, SETTINGS_*); remove their
  ALLOWED_CONFIG_KEYS entries and the orphaned settings-site UI fields
- configSchema: delete unreachable json/string_or_json validators
- models: drop unused ticketTag field
- gmail-poll: remove unused isPollSuspended export
- utils: remove dead htmlToTextWithBlocks/decodeHtmlEntities/BLOCK_TAG_REGEX
- internalApi: remove router._allowedKeys (test it served is gone)
- discord client: drop unused GuildPresences privileged intent
- broccolini-discord: remove dormant /api 503 gate (no /api routes)

Fixes:
- context-menu ticket create now uses makeTicketName('unclaimed', ...)
  instead of the contract-violating ticket-<n> name
- drop write-only pending.userId from both close paths

Dedup / simplify:
- new services/transcript.js shares the transcript text/date/header
  builders between the button and force-close paths (had drifted)
- resolveEscalationCategoryId() replaces 3 copies of the category logic
- ticketChannelOverwrites() shares the create-permission array between
  the two interactive ticket-create paths
- finalizeBody() shares the email-cleanup tail in parseGmailMessage
- getTicketActionRow drops its never-passed options arg;
  sendTicketNotificationEmail drops its always-null subjectLine arg
- hoist invariant guild lookup out of the auto-close/unclaim loops
- drop redundant lastActivity write (and now-dead updateTicketActivity)
- /help lists all current commands and the right-click apps
2026-06-02 19:59:14 +00:00
2152544d09 escalation/de-escalation: keep ticket creator and /add'd users on the channel
enqueueMove called channel.setParent(categoryId, { lockPermissions: true }).
discord.js's default for setParent is also true. With lockPermissions: true,
Discord re-syncs the channel's permission overwrites to match the new
parent category — so the explicit per-user allows set at ticket creation
(creator) and via /add (extra members) got wiped on every escalate,
de-escalate, and /move. The creator literally lost View Channel on their
own ticket the moment staff hit Escalate.

Flip to lockPermissions: false so the existing channel-level overwrites
are preserved across the parent change. Inheritance still applies for
anything the channel doesn't override — and the deny-@everyone /
role-allow set at creation continues to gate access correctly.

Affects every caller of enqueueMove:
  - handlers/commands/escalation.js runEscalation
  - handlers/commands/escalation.js runDeescalation
  - handlers/commands/index.js handleMove (/move)
2026-05-19 20:09:58 +00:00
452f005aea silence secondary-bot 429 fallback noise from debug channel
Two paired logWarn calls used to post to DEBUGGING_CHANNEL_ID every time
the RENAMER_BOT secondary token hit Discord's per-channel rename quota:

  Warning: renamer        — "429 rename channel=… retry_after=…"
  Warning: renameQueue    — "secondary-bot 429; falling back to primary…"

Both fire on the recoverable path: the channelQueue immediately falls
back to the primary discord.js client, and that client's REST handler
transparently waits out the retry_after and retries — the rename lands
without operator action. Posting these to the debug channel was pure
noise; staff were reading them as failures when nothing had failed.

Demoted both to console.warn so they still appear in `docker logs
broccolini` for diagnostic purposes but no longer post to Discord.

Kept untouched:
- utils/renamer.js:64 — 401/403 logWarn on secondary-bot auth/permission
  errors (real config problems; the operator does need to know).
- services/channelQueue.js next.catch logError for status 401/403/429 —
  only fires when the fallback itself also failed (rare and worth a
  debug-channel post).
2026-05-19 18:38:18 +00:00
3c13e55dad audit week 3 quality batch: QUAL-004/005/007/008/010 + SEC-002
QUAL-004 handlers/messages.js — DM-on-customer-reply now reads
guild.members.cache.get(claimerId) first and only falls back to
guild.members.fetch on cache miss. Avoids a REST round-trip per non-staff
reply on busy tickets. GuildMembers intent already keeps the cache warm.

QUAL-005 handlers/buttons.js (runFinalClose) + handlers/commands/close.js
(finalizeForceClose) — close paths now $unset welcomeMessageId alongside
the status: 'closed' write. Stops a stale message-ID from carrying into a
future reopen on the same Gmail thread, where escalation's "edit welcome
buttons" path would silently fail trying to fetch a message in a deleted
channel.

QUAL-007 services/configPersistence.js — writeEnvFile mismatch error now
includes the missing/extra key sets, not just count vs count. Saves the
operator from guessing which key vanished after a partial write.

QUAL-008 utils.js stripEmailQuotes — replaced order-dependent first-match
loop with an earliest-match-across-all-markers scan. The previous code
could truncate at a late "_____" signature underline even when an earlier
"On X wrote:" reply header was the real cutoff. New test in
tests/utils.test.js exercises the dual-marker case.

QUAL-010 broccolini-discord.js — moved `let httpServer / internalServer /
appReady` declarations from after the ready handler to before it. Same
runtime behavior (module-load completes before ready fires asynchronously),
but the read order now matches the assignment order.

SEC-002 routes/internalApi.js — POST /restart now goes through a tighter
2/min limiter on top of the shared 10/min internalLimiter. Defense in
depth in case INTERNAL_API_SECRET ever leaks; an attacker with the secret
can no longer crash-loop the container.

Skipped: QUAL-009 (re-checked the regex; ^\s*\n* → \n is already
idempotent — the audit finding was incorrect).

vitest run: 88/88 (one new test for QUAL-008).
2026-05-08 20:46:04 +00:00
3e9ad658d0 audit week 3 [SEC-004 + SEC-005]: scope members.fetch + redact PII in debug logs
[SEC-004] services/staffThread.js — addRoleMembersToThread previously called
the unscoped guild.members.fetch() on every ticket creation, chunking every
member of the guild. With STAFF_THREAD_AUTO_ADD_ROLE on and a 50-member
staff role, the 300ms-per-add loop also blocked ticket creation for ~15s.

  - Read role.members directly (computed from guild.members.cache, kept in
    sync via the GuildMembers gateway intent set on the client). Skip the
    explicit unscoped fetch in the hot path.
  - Cache-cold fallback: one scoped guild.members.fetch({ withPresences:
    false }) — irrelevant presence sync stays off the wire.
  - createStaffThread no longer awaits the add-loop. Wraps the call in
    setImmediate(...) so ticket creation returns immediately while the
    rate-limited add-loop runs in the background.

[SEC-005] services/debugLog.js — stacks/messages posted to the debug
channel could leak customer email addresses (interpolated through ticket
errors) and Discord member/channel IDs. Add a redactPII helper applied to
both logError's message + stack and logWarn's body:

  - Email regex /[\w.+-]+@[\w.-]+\.\w+/g → [EMAIL_REDACTED]
  - Discord snowflake /\b\d{18,20}\b/g → [ID_REDACTED]

interaction.user.tag in the User: line is intentionally not redacted —
it's needed for triage and is not PII (Discord usernames are public).
2026-05-08 20:42:48 +00:00
cdf85f6364 audit week 1: creator ID tracking, channel-queue migration, deprecation cleanup
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.
2026-05-08 20:19:14 +00:00
83b6b4ae0c simplify: rename CONFIG channels, dedup hasStaffRole, drop enforceEmbedLimit
- 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>
2026-05-07 18:45:18 +00:00
840b6bfcf8 simplify: prune dead code, dedup gmail send, drop neutered log stubs
- 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>
2026-05-07 18:37:14 +00:00
d1e1408256 email fixes 2026-04-21 20:54:49 +00:00
8e362c607d mvp: neuter debug log helpers, strip config keys, update tagline 2026-04-21 20:18:59 +00:00
ce62b7a94a cleanup: untrack .bak3 backups; dedupe + broaden bak gitignore pattern 2026-04-21 17:26:35 +00:00
f3ee27ed7a more mvp strip 2026-04-21 17:24:03 +00:00
34dc55c20b strip: remove /backup /export /search /stats /fix-stale-tickets + analytics module
- delete handlers/analytics.js
- remove trackInteraction calls; replace trackError with logError().catch(() => {})
- remove 5 slash commands from register.js
- remove BACKUP_EXPORT_CHANNEL_ID from config + schema + .env.example
2026-04-21 16:44:01 +00:00
fa7d4af132 strip: delete stale docs/ and broccolini_bot_context.md
Both were saturated with references to removed features.
Regenerate fresh post-MVP.
2026-04-21 16:32:05 +00:00
bf901039bc mvp & email signature 2026-04-21 16:15:18 +00:00
1a46fb696a cleanup: remove strip backup files 2026-04-21 15:57:51 +00:00
636348d824 strip: remove pattern/surge/chat alert monitoring + unused commands
- delete services/{patternChecker,patternStore,surgeChecker,chatAlertChecker,staffNotifications,staffChannel,notificationRegistry,notificationEnabled,staffPresence}.js
- remove /notification, /staffnotification, /tag, /priority
- /escalate: drop action param, always unclaim
- purge PATTERN_*, SURGE_*, CHAT_ALERT_*, STAFF_* env vars from config + .env.example
- drop StaffNotification model
- ~2500 LOC removed
- settings-site /internal/notifications/* endpoints gone (UI will 404 until trimmed)
2026-04-21 15:57:18 +00:00
74d7f49c8d test 2026-04-21 14:32:34 +00:00
33b1f276c6 audit 2026-04-20 18:05:36 +00:00
d73422555d rename path: fix env-var mismatch, gut canRename gate, add primary-bot fallback on 401/403/429
- 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>
2026-04-20 15:56:06 +00:00
fcce7c3e86 changes 2026-04-20 14:56:55 +00:00
8a45b59b28 phase 9 notification toggles (per-alert, per-category, master; default-disabled) 2026-04-18 23:51:59 +00:00
39a5482516 phase 8 server-side validation (configSchema, inline field errors, partial-success semantics) 2026-04-18 19:54:47 +00:00
0f62fb9020 phase 5 dynamic alert registry (bot canonical, settings-site with fallback) 2026-04-18 19:14:51 +00:00
21618efbad security hardening 2026-04-18 11:10:41 +00:00
a409203025 manual commit 2026-04-10T20:51:10Z 2026-04-10 20:51:10 +00:00
785b2e5b8f manual commit 2026-04-10T20:31:52Z 2026-04-10 20:31:52 +00:00
cda5019918 manual commit 2026-04-10T20:16:18Z 2026-04-10 20:16:18 +00:00
f8d323b0c7 manual commit 2026-04-10T20:01:57Z 2026-04-10 20:01:57 +00:00
71d6e0a045 manual commit 2026-04-10T19:57:09Z 2026-04-10 19:57:09 +00:00
4426c4ee0f manual commit 2026-04-10T19:45:00Z 2026-04-10 19:45:00 +00:00
1017ef6ae7 manual commit 2026-04-10T19:27:53Z 2026-04-10 19:27:53 +00:00
indifferentketchup
eae801ff7d queue 2026-04-09 14:57:41 -05:00
indifferentketchup
7fff9192b4 queue 2026-04-09 09:49:19 -05:00
indifferentketchup
a4fb82620a notification changes 2026-04-08 09:22:47 -05:00
indifferentketchup
03794ceb25 scan for deleted tickets 2026-04-07 10:15:58 -05:00
indifferentketchup
56ba8e363a changes 2026-04-07 09:29:24 -05:00
indifferentketchup
69c247ed1b huge changes 2026-04-07 01:43:06 -05:00
indifferentketchup
c5d7539677 staff notifications 2026-04-06 23:53:32 -05:00
indifferentketchup
4b984312a8 change in ticket renaming and flow 2026-04-06 16:37:50 -05:00
indifferentketchup
1496a96274 Dynamic overflow categories 2026-03-28 20:55:36 -05:00
indifferentketchup
6b4fd65d4b personal queue 2026-03-28 20:07:17 -05:00
indifferentketchup
8a4e306f28 p-queue 2026-03-28 18:39:00 -05:00
samkintop
29a13768f7 Sync broccolini-bot: rename from zammad, docs in docs/, security gitignore, remove zammad deps
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 02:56:00 -06:00
root
519788c633 Initial commit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 08:22:19 -06:00