1
0
Fork 0

docs: document the Friday open-slot notice and roll-call embed updates

This commit is contained in:
Jason Fraley 2026-09-18 14:52:39 -04:00
parent fd9f36fc46
commit 8104b500b9
3 changed files with 21 additions and 4 deletions

View file

@ -323,8 +323,8 @@ Five XP-earning minigames under one parent: routes live at `/minigames/*` (moved
A Discord bot living in `src/bot/`, run as a standalone long-running process via `bun run bot` — it imports `@payload-config` directly (same pattern as `src/scripts/` and `src/tools/seed/`) and shares the Postgres DB with the web app. **Under active development and testing.** **Full product requirements live in `docs/bot/context.md`; the approved implementation design is in `docs/bot/design.md` — read both before touching bot code.** A Discord bot living in `src/bot/`, run as a standalone long-running process via `bun run bot` — it imports `@payload-config` directly (same pattern as `src/scripts/` and `src/tools/seed/`) and shares the Postgres DB with the web app. **Under active development and testing.** **Full product requirements live in `docs/bot/context.md`; the approved implementation design is in `docs/bot/design.md` — read both before touching bot code.**
- **Env / run**: requires `DISCORD_TOKEN` and `DISCORD_GUILD_ID` (fail-fast on missing required vars in `src/bot/config.ts`); optional `DISCORD_OPS_CHANNEL_ID`, `DISCORD_ANNOUNCE_CHANNEL_ID`, `DISCORD_STAFF_ROLE_IDS`, `DISCORD_ATTENDANCE_POLL_MS` (default 60s), `DISCORD_NOTIFICATION_POLL_MS` (default 20s), `DISCORD_EVALUATION_POLL_MS` (default 60s), `DISCORD_IMMINENT_POLL_MS` (default 60s), plus existing `APP_URL`. Logs through `payload.logger`. - **Env / run**: requires `DISCORD_TOKEN` and `DISCORD_GUILD_ID` (fail-fast on missing required vars in `src/bot/config.ts`); optional `DISCORD_OPS_CHANNEL_ID`, `DISCORD_ANNOUNCE_CHANNEL_ID`, `DISCORD_STAFF_ROLE_IDS`, `DISCORD_ATTENDANCE_POLL_MS` (default 60s), `DISCORD_NOTIFICATION_POLL_MS` (default 20s), `DISCORD_EVALUATION_POLL_MS` (default 60s), `DISCORD_IMMINENT_POLL_MS` (default 60s), `DISCORD_SIDE_OPS_CHANNEL_ID` (Friday open-slot notice channel), `DISCORD_COMMUNITY_ZEUS_ROLE_ID` (role ping for that notice), `DISCORD_SIDE_OPS_POLL_MS` (default 30 min), plus existing `APP_URL`. Logs through `payload.logger`.
- **Structure**: `commands/` — `ping`, `signup`, `link`, `announce`, `remindEvaluations`; `events/interactionCreate.ts` — routes `ptf-att:` RSVP buttons; `services/` — `missionEmbeds` (attendance embed lifecycle + reconcile loop), `notificationBridge` (poll → Discord DMs), `evaluationReminders` (post-mission evaluation reminder DMs), and `imminentReminders` (pre-op thread + attendee pings); `lib/` — `roles.ts` (`isStaff`), `resolve.ts` (discordId ↔ Payload user lookups). Command registration scope: `signup`/`link`/`ping` are **global** (DM-usable — guild-scoped commands never appear in DMs), `announce`/`remind-evaluations` are guild-only. - **Structure**: `commands/` — `ping`, `signup`, `link`, `announce`, `remindEvaluations`; `events/interactionCreate.ts` — routes `ptf-att:` RSVP buttons; `services/` — `missionEmbeds` (attendance embed lifecycle + reconcile loop), `notificationBridge` (poll → Discord DMs), `evaluationReminders` (post-mission evaluation reminder DMs), `imminentReminders` (pre-op thread + attendee pings), and `fridayOpsNotice` (weekly #side-ops ping when the upcoming Friday has no op, edited to credit the community claimer); `lib/` — `roles.ts` (`isStaff`), `resolve.ts` (discordId ↔ Payload user lookups). Command registration scope: `signup`/`link`/`ping` are **global** (DM-usable — guild-scoped commands never appear in DMs), `announce`/`remind-evaluations` are guild-only.
- **Evaluation reminders**: once a mission is evaluable (`Completed`, or `Scheduled`/`Active` past its start — same rule as `isMissionEvaluable`), the bot sends a one-shot DM (per user with a linked `discordId`) asking them to rate their leadership, plus a "rate your subordinates" section for leaders whose members RSVP'd yes. Links go to each ratee's profile page where the rating dialog lives. Recipient computation: `computeEvaluationReminderPlan` in `src/lib/evaluations/reminders.ts`; state marker: `evaluationRemindersSentAt` on Missions (one-shot, same pattern as `discordAttendanceSentAt`; withheld if the 40-DM/tick cap is hit — retries next tick). These DMs deliberately ignore `preferences.discord.enabled` (defaults false, not yet exposed in the web UI — honoring it would DM nobody). Staff can trigger manually via `/remind-evaluations` (optional `mission` option accepts an ID, name, or code name and re-sends even if the marker is set; omitted, it sweeps all pending missions and reports `{processed, sent}`). - **Evaluation reminders**: once a mission is evaluable (`Completed`, or `Scheduled`/`Active` past its start — same rule as `isMissionEvaluable`), the bot sends a one-shot DM (per user with a linked `discordId`) asking them to rate their leadership, plus a "rate your subordinates" section for leaders whose members RSVP'd yes. Links go to each ratee's profile page where the rating dialog lives. Recipient computation: `computeEvaluationReminderPlan` in `src/lib/evaluations/reminders.ts`; state marker: `evaluationRemindersSentAt` on Missions (one-shot, same pattern as `discordAttendanceSentAt`; withheld if the 40-DM/tick cap is hit — retries next tick). These DMs deliberately ignore `preferences.discord.enabled` (defaults false, not yet exposed in the web UI — honoring it would DM nobody). Staff can trigger manually via `/remind-evaluations` (optional `mission` option accepts an ID, name, or code name and re-sends even if the marker is set; omitted, it sweeps all pending missions and reports `{processed, sent}`).
- **Sign-up / linking (feature 1)**: `/signup` is **DM-only** (the temp password flows through the DM). Creates the Payload user with `username` = `discordUsername` = the caller's Discord username, plus `discordId`, `displayName`, `steamId`, and a random temp password. The ephemeral reply carries the password as the guaranteed delivery path; `interaction.user.send()` is a best-effort persistent copy, so a blocked DM never orphans the account. `/link` works in servers **and** DMs (credential-free, ephemeral reply only) — matches `discordUsername` → sets `discordId`. - **Sign-up / linking (feature 1)**: `/signup` is **DM-only** (the temp password flows through the DM). Creates the Payload user with `username` = `discordUsername` = the caller's Discord username, plus `discordId`, `displayName`, `steamId`, and a random temp password. The ephemeral reply carries the password as the guaranteed delivery path; `interaction.user.send()` is a best-effort persistent copy, so a blocked DM never orphans the account. `/link` works in servers **and** DMs (credential-free, ephemeral reply only) — matches `discordUsername` → sets `discordId`.
- **DM gotcha**: a user with "Allow direct messages from server members" off in Discord privacy settings can neither receive the bot's DMs nor open a DM with the bot. The `/signup` rejection message explains how to enable it. - **DM gotcha**: a user with "Allow direct messages from server members" off in Discord privacy settings can neither receive the bot's DMs nor open a DM with the bot. The `/signup` rejection message explains how to enable it.

View file

@ -112,7 +112,7 @@ Dependency: `discord.js` ^14 (add to root `package.json`). Script: `"bot": "bun
- `setMissionAttendance(...)` → immediately re-render + edit the embed (no waiting for the poll), update hash → ephemeral confirmation. - `setMissionAttendance(...)` → immediately re-render + edit the embed (no waiting for the poll), update hash → ephemeral confirmation.
3. **Web → Discord**: web action → `setMissionAttendance` → next reconcile tick sees a hash difference → edits the embed. **Loop safety**: editing the embed never mutates attendance data, so the hash stabilizes — no feedback loop. Hash stored on the mission doc makes reconcile stateless across bot restarts (two new optional text fields on missions: `discordMessageId`, `discordAttendanceHash`). 3. **Web → Discord**: web action → `setMissionAttendance` → next reconcile tick sees a hash difference → edits the embed. **Loop safety**: editing the embed never mutates attendance data, so the hash stabilizes — no feedback loop. Hash stored on the mission doc makes reconcile stateless across bot restarts (two new optional text fields on missions: `discordMessageId`, `discordAttendanceHash`).
**Embed contents**: mission name + codeName, `<t:epoch:F>` absolute + `<t:epoch:R>` relative start time, operationType, maxPlayers, and three sections — ✅ Yes (n), 🤔 Tentative (n), ❌ No (n) — with member display names. **Embed contents**: mission name + codeName (title links to the mission page), mission summary below the title (truncated to 300 characters with an ellipsis), `<t:epoch:F>` absolute + `<t:epoch:R>` relative start time, operationType, maxPlayers, and three sections — ✅ Yes (n), 🤔 Tentative (n), ❌ No (n) — with member display names, plus an "Open the op" field with the mission-page URL. `attendanceHash` carries an embed schema version so layout-only changes force one idempotent re-render of already-live roll-calls.
### C. Notifications bridge + announcements (feature 3) ### C. Notifications bridge + announcements (feature 3)
@ -135,6 +135,14 @@ Dependency: `discord.js` ^14 (add to root `package.json`). Script: `"bot": "bun
- **Action**: the bot opens a thread on the roll-call message ("Op starting soon: <title>", 100-char cap) and posts a reminder embed with the relative start time (`<t:...:R>`) plus a link to the mission page, pinging every yes-RSVP via `<@discordId>`. Pings ignore `preferences.discord.enabled` (same deliberate exception as evaluation reminders: a yes RSVP is explicit intent to attend). Unlinked attendees are skipped; a thread with no pings is still posted so players have a check-in point. - **Action**: the bot opens a thread on the roll-call message ("Op starting soon: <title>", 100-char cap) and posts a reminder embed with the relative start time (`<t:...:R>`) plus a link to the mission page, pinging every yes-RSVP via `<@discordId>`. Pings ignore `preferences.discord.enabled` (same deliberate exception as evaluation reminders: a yes RSVP is explicit intent to attend). Unlinked attendees are skipped; a thread with no pings is still posted so players have a check-in point.
- **Staleness bound**: a missed reminder still fires within 15 minutes after the start (bot-restart grace), never later; the cancel flow clears the marker so a rescheduled op re-reminds for its new start time. - **Staleness bound**: a missed reminder still fires within 15 minutes after the start (bot-restart grace), never later; the cancel flow clears the marker so a rescheduled op re-reminds for its new start time.
### F. Friday open-slot notice (pre-Friday)
- **Trigger**: at 8 PM Eastern on Wednesday (evaluated in `America/New_York`, so the deploy container's own timezone is irrelevant), when no non-cancelled mission (any status, any operation type) starts on that week's upcoming Friday. A draft (Concept) already counts as "someone is on the slot". Poll loop every `DISCORD_SIDE_OPS_POLL_MS` (default 30 min), so the post lands within half an hour of 8 PM ET. A restart later in the evening still catches up until ET midnight; a fully missed Wednesday waits for the following week's notice.
- **Action**: one message in the #side-ops channel (`DISCORD_SIDE_OPS_CHANNEL_ID`) pinging the Community Zeus role, saying the Friday slot is open, they are welcome to host an op (link to the Friday Ops calendar), and that if nobody picks it up the slot stays empty or Jason hosts it himself.
- **Role resolution**: `DISCORD_COMMUNITY_ZEUS_ROLE_ID` env if set, otherwise the bot resolves the role by name ("Community Zeus") in the guild cache. Neither available: the notice posts without the role ping and a warn is logged.
- **Dedup**: no DB state. The notice embeds a stable per-Friday `<t:epoch:D>` token; before posting, the service scans the last 100 channel messages for it, so restarts never double-post.
- **Claim credit**: when a non-cancelled mission later appears on that Friday whose creator is not a superuser (`getFridayClaimant` in `src/lib/intelligence/fridayOpsNotice.ts`, earliest `createdAt` wins; superuser- or staff-created missions never claim), the service edits the notice in place, appending "This slot was claimed by \<user\>. Thank you for hosting for our community!". If the claiming op is later cancelled or deleted, the claim line is stripped and the notice reverts to the open-slot state (a claim swap between polls re-credits the new claimant). Idempotent via a claim-line check on the message content; the edit half runs whenever the notice is still in the recent-messages window, regardless of day, so a Thursday claim still lands.
### Authz (`src/bot/lib/roles.ts`) ### Authz (`src/bot/lib/roles.ts`)
`isStaff(member)`: caller has any `DISCORD_STAFF_ROLE_IDS`, **or** their linked Payload user has roles `admin`/`developer`. RSVP/signup/link are open to all members. The bot performs Payload writes with `overrideAccess` and relies on these checks + its own validations. `isStaff(member)`: caller has any `DISCORD_STAFF_ROLE_IDS`, **or** their linked Payload user has roles `admin`/`developer`. RSVP/signup/link are open to all members. The bot performs Payload writes with `overrideAccess` and relies on these checks + its own validations.
@ -152,6 +160,9 @@ Dependency: `discord.js` ^14 (add to root `package.json`). Script: `"bot": "bun
| `DISCORD_NOTIFICATION_POLL_MS` | no | default 20000 | | `DISCORD_NOTIFICATION_POLL_MS` | no | default 20000 |
| `DISCORD_EVALUATION_POLL_MS` | no | default 60000 | | `DISCORD_EVALUATION_POLL_MS` | no | default 60000 |
| `DISCORD_IMMINENT_POLL_MS` | no | default 60000 | | `DISCORD_IMMINENT_POLL_MS` | no | default 60000 |
| `DISCORD_SIDE_OPS_CHANNEL_ID` | no | #side-ops channel for the Friday open-slot notice |
| `DISCORD_COMMUNITY_ZEUS_ROLE_ID` | no | Community Zeus role ping (falls back to resolving the role by name) |
| `DISCORD_SIDE_OPS_POLL_MS` | no | default 1800000 (30 min) |
`APP_URL` already exists — used for login links in DMs. `APP_URL` already exists — used for login links in DMs.

View file

@ -31,6 +31,7 @@ bot/
notificationBridge.ts # Poll user-notifications → Discord DMs notificationBridge.ts # Poll user-notifications → Discord DMs
evaluationReminders.ts # Post-mission evaluation reminder DMs (one-shot marker on Missions) evaluationReminders.ts # Post-mission evaluation reminder DMs (one-shot marker on Missions)
imminentReminders.ts # Pre-op reminder: thread on the roll-call message + attendee pings (one-shot marker on Missions) imminentReminders.ts # Pre-op reminder: thread on the roll-call message + attendee pings (one-shot marker on Missions)
fridayOpsNotice.ts # Weekly notice: pings Community Zeus in #side-ops when the upcoming Friday has no op
signup.ts # Signup service logic signup.ts # Signup service logic
transferRequests.ts # Polls assignment transfer requests awaiting leader decision transferRequests.ts # Polls assignment transfer requests awaiting leader decision
transferDelivery.ts # Transfer leader-request / rejection / info delivery DMs transferDelivery.ts # Transfer leader-request / rejection / info delivery DMs
@ -52,7 +53,7 @@ bot/
## Feature flow: attendance ## Feature flow: attendance
Bot posts RSVP embeds (Yes/Tentative/No) for future, Ready/Scheduled, visibility:"unit" missions into ops channel. Stores `discordMessageId` + `discordAttendanceHash` on mission. Reconciles hash changes every poll tick (web ↔ Discord two-way sync). Web UI: `MissionAttendance` component. Bot posts RSVP embeds (Yes/Tentative/No) for future, Ready/Scheduled, visibility:"unit" missions into ops channel. Stores `discordMessageId` + `discordAttendanceHash` on mission. Reconciles hash changes every poll tick (web ↔ Discord two-way sync). The embed title links to the mission page, a mission summary sits under the title (truncated to 300 characters with an ellipsis), and an "Open the op" field carries the URL; `attendanceHash` includes an embed schema version so layout-only changes re-render live roll-calls once (idempotent). Web UI: `MissionAttendance` component.
## Feature flow: notifications ## Feature flow: notifications
@ -66,6 +67,10 @@ Once a mission is evaluable (`Completed`, or `Scheduled`/`Active` past its start
15 minutes before a posted op starts (`visibility: "unit"`, Ready/Scheduled, live roll-call message), `imminentReminders` opens a thread on the roll-call message ("Op starting soon: <name>") and pings every yes-RSVP via `<@discordId>` (pings ignore `preferences.discord.enabled`, same exception as evaluation reminders). One-shot via the `imminentReminderSentAt` marker on Missions; the cancel flow clears it so a rescheduled op re-reminds. Grace bound: a missed reminder still fires within 15 minutes after the start, never later. Query: `src/lib/intelligence/imminentReminders.ts` (window constants + `getImminentReminderMissions`). 15 minutes before a posted op starts (`visibility: "unit"`, Ready/Scheduled, live roll-call message), `imminentReminders` opens a thread on the roll-call message ("Op starting soon: <name>") and pings every yes-RSVP via `<@discordId>` (pings ignore `preferences.discord.enabled`, same exception as evaluation reminders). One-shot via the `imminentReminderSentAt` marker on Missions; the cancel flow clears it so a rescheduled op re-reminds. Grace bound: a missed reminder still fires within 15 minutes after the start, never later. Query: `src/lib/intelligence/imminentReminders.ts` (window constants + `getImminentReminderMissions`).
## Feature flow: Friday open-slot notice
At 8 PM Eastern on Wednesday (`America/New_York`, evaluated via `isNoticeDue` in `src/lib/intelligence/fridayOpsNotice.ts`; catch-up until ET midnight, a fully missed Wednesday skips to next week), if no non-cancelled mission starts on that week's upcoming Friday, `fridayOpsNotice` posts once in #side-ops pinging the Community Zeus role: the slot is open, hosting is welcome, and if nobody picks it up the slot stays empty or Jason hosts it himself. When a non-superuser creator later claims the slot (`getFridayClaimant`, earliest-created mission wins; superuser/staff-created missions never claim), the notice is edited to append "This slot was claimed by \<user\>. Thank you for hosting for our community!"; if that op is later cancelled or deleted, the claim line is stripped and the notice reverts to the open-slot state (a between-polls claim swap re-credits the new claimant). Role: `DISCORD_COMMUNITY_ZEUS_ROLE_ID` or resolved by name in the guild; channel: `DISCORD_SIDE_OPS_CHANNEL_ID`. Dedup without DB state: the message embeds a stable per-Friday `<t:epoch:D>` token and the service scans recent channel messages for it (claim idempotency via a marker in the message content). Drafts (Concept) count as the slot being taken.
## Feature flow: assignment transfers ## Feature flow: assignment transfers
`transferRequests` polls `assignment-transfers` for requests awaiting leader decision; leaders approve/deny via buttons handled in `transferInteractions.ts`; DMs are built by `lib/transferMessaging.ts` and sent through `transferDelivery.ts` (leader request, requester rejection notice, decision info). Web side: `src/lib/transfers/` + `/transfers` page. `transferRequests` polls `assignment-transfers` for requests awaiting leader decision; leaders approve/deny via buttons handled in `transferInteractions.ts`; DMs are built by `lib/transferMessaging.ts` and sent through `transferDelivery.ts` (leader request, requester rejection notice, decision info). Web side: `src/lib/transfers/` + `/transfers` page.
@ -80,6 +85,7 @@ Once a mission is evaluable (`Completed`, or `Scheduled`/`Active` past its start
| Change DM bridging | `bot/services/notificationBridge.ts` | | Change DM bridging | `bot/services/notificationBridge.ts` |
| Evaluation reminder logic | `bot/services/evaluationReminders.ts` + `src/lib/evaluations/reminders.ts` | | Evaluation reminder logic | `bot/services/evaluationReminders.ts` + `src/lib/evaluations/reminders.ts` |
| Imminent-op reminder logic | `bot/services/imminentReminders.ts` + `src/lib/intelligence/imminentReminders.ts` | | Imminent-op reminder logic | `bot/services/imminentReminders.ts` + `src/lib/intelligence/imminentReminders.ts` |
| Friday open-slot notice | `bot/services/fridayOpsNotice.ts` + `src/lib/intelligence/fridayOpsNotice.ts` |
| Transfer workflow | `bot/services/transferRequests.ts` + `lib/transferMessaging.ts` | | Transfer workflow | `bot/services/transferRequests.ts` + `lib/transferMessaging.ts` |
| User lookup patterns | `bot/lib/resolve.ts` | | User lookup patterns | `bot/lib/resolve.ts` |