Files
minabot/src/discord
..

Discord interface

Signed HTTP interactions enter /api/discord/interactions. The raw request body is verified with the application's Ed25519 public key and a five-minute timestamp window. DISCORD_PUBLIC_KEY enables the endpoint; DISCORD_CLIENT_ID identifies the application. Commands are global with contexts [0, 1] (server and bot DM).

HabitService remains the shared domain implementation. Verified Discord user IDs resolve the same account as website OAuth; new accounts start in UTC. Server responses are ephemeral. Buttons/forms carry opaque persisted owner-bound state, expire after 15 minutes, and mutation controls are consumed after successful use. Interaction IDs retain replay receipts for 24 hours. Neither table stores tokens.

daily.ts implements the /today workflow. types.ts, ui.ts, and state.ts provide the shared feature building blocks. Feature tasks extend command registration and route dispatch while keeping their behavior in separate modules.

Tests use real Ed25519 signatures and disposable migrated SQLite databases. The foundation tests cover server/DM privacy, replay after restart, stale progress, cross-account controls, and matching website state. All feature modules are now composed in the shared API. End-to-end user interaction in Discord remains a manual acceptance check after deployment and registration.

/help lists the current command definitions automatically, explains check-in controls and privacy, and links to the website. It works before account setup and does not create an account or synchronize habits. Re-run the registration script with --apply after deployment to expose newly added commands in Discord. Reminder delivery accepts commandsAvailable; production enables it when DISCORD_PUBLIC_KEY is a 64-digit hexadecimal key and DISCORD_CLIENT_ID is set. The existing bot token still controls delivery. Configured reminders include an owner-bound today control with mode: 'reminder': it opens a separate private flow, leaving the reminder unchanged, and expires at the sending account's next local midnight (including DST). Delivery claims, nonce, opt-out, and retry rules are unchanged. The dashboard preview explicitly disables buttons because it has no signed interaction endpoint configured. Global command registration and the Discord interaction endpoint must be configured separately before production use.

Preferences

preferences.ts implements global /settings and /reminders. Reminder panels paginate six named schedules at a time; entries with matching name and schedule are edited together as multiple comma-separated 24-hour times. Forms accept weekday names (or every day) or one ISO calendar date. Delivery is toggled for all schedules. Removing the last schedule disables delivery and stores an empty list; the shared contract permits empty lists only while disabled. Invalid forms provide a prefilled retry button. UTC accounts receive a timezone setup hint.

ReminderService is shared with the website routes. Every save advances its updatedAt monotonically; Discord controls compare the saved preference snapshot and the user's timezone/update version before editing. HabitService.setTimezone also advances the user timestamp monotonically, including repeated writes in the same millisecond. This rejects old Discord forms after edits from either interface, including change-and-revert sequences. Website PUT retains its existing request contract. Delivery receipts do not invalidate preference controls.

Integration: retain the preferences command dispatch and preferencesAction component dispatch in routes.ts, the two definitions in commands.ts, and the preference fields in ControlData. CommandConfig.remindersAvailable supplies availability; createApi derives it from the existing sharing bot token, matching the website and reminder worker. Direct route fixtures default it to false. No new environment variables, migrations, outbound requests, or remote command registration are needed by this module. Global command registration remains an integration step. Successful mutations invoke the supplied live-card dirty hook. progress.ts implements /progress: six-habit component pages, focused private previews, explicit destination confirmation for snapshots, and live-image start/status/pause/resume. API composition injects the same sharing.send and live.start/pause/state operations used by website routes. Keep these shared instances when reconciling dispatch; do not construct a second live worker.

Progress rendering and network actions acknowledge existing message interactions with type 6 and edit the original message once; a flow without a source message uses type 5 (ephemeral in servers). DeferredDispatch.complete and schedule are injectable for tests. Interaction tokens live only in memory, never receipts or controls. A process interruption after acknowledgement can leave the private response unfinished; restarting never replays the public send. Run /progress again to inspect status. Public snapshot and live deliveries retain their existing persistent quotas, nonce and uncertain-delivery guards.

progress-image.ts renders the focused 30-day snapshot at 1200×860 with bundled Mina Sans / Instrument Serif, shared theme colors, and existing habit shades. The 30 cells retain fixed readable sizes; symbols are Canvas paths. Text summaries and buttons accompany the PNG. To produce review images without sending messages: PROGRESS_IMAGE_REVIEW_DIR=/tmp/minabot-progress-review bun test src/discord/progress-image.test.ts.

Integrated commands

All commands use global registration with server and bot-DM contexts. /help reads this same registration metadata, so its command list stays in sync.

Command Function
/help Command guide and website link
/today Daily manual/count/task check-ins
/habits Create, edit, schedule, archive, restore, permanently delete
/tasks Add, rename, schedule, delete routine tasks
/history Inspect and correct dated progress, including archived habits
/reminders Named schedules, multiple times, quiet hours, enable/disable
/settings Timezone and reminder delivery status
/progress Private graphics, confirmed snapshot sharing, live-image controls

After the deployment controller installs a release, register its definitions:

cd /opt/minabot
sudo docker compose exec -T app bun scripts/register-discord-commands.ts --apply

Registration upserts individual global commands and preserves unrelated commands. The public key and interactions URL are one-time setup; adding commands requires rerunning registration after their handlers have deployed.

Additional command graphics

/today includes View day card, summarizing all due habits in pages of six. The selected daily check-in and /history editor include View check-in card, showing the selected date's requirements, task completion, carryover where applicable, and a 14-day progress strip ending on that date. /reminders includes View schedule card, showing each time slot, weekday/date schedule, quiet-hour conflicts, and delivery status. Schedule cards retain disabled/unavailable states. Management uses native component panels and forms. /help remains text-based.

These are private snapshots: Refresh card, Toggle theme, and pagination request a fresh owned snapshot. Regular check-ins use immediate component updates. Graphic actions acknowledge before rendering and attach a PNG to a new private reply; they never publish to the sharing channel or write progress. Each image has a text equivalent and attachment description. A render failure retains the text and navigation. No new command registration or configuration is needed after deploying this change.

Generate local visual review artifacts with COMMAND_IMAGE_REVIEW_DIR=/tmp/minabot-command-review bun test src/discord/command-graphics.test.ts.

Interactive daily dashboard

/today uses Discord Components V2 containers with existing habit accent colors, a daily completion summary, and unfinished habits first. Five habits fit on each page within Discord's 40-component limit. Manual habits have inline completion and undo actions; counts have inline +1; routines open a checklist with individual Complete/Undo buttons. Details retain exact amounts, carryover, and amount forms. Checklists paginate eight tasks at a time.

Navigation, saves, and recoverable daily validation errors update the originating message (including amount-modal submissions). Updates clear legacy content, embeds, and attachments when upgrading an older message to V2. Server command replies remain ephemeral; reminder buttons open a separate private flow. Optional PNG cards still open as separate snapshots. Historical editors reuse the same component check-in screens while retaining their dated requirements.

The shared HabitService, owner-bound controls, stale snapshot checks, replay receipts, and local-midnight protections remain authoritative. No schema change, new API, or command registration is required. Native Discord mobile/desktop layout and real modal return behavior should be checked manually after deployment.

Component command screens

/habits, /history, /reminders, and /progress share the native screen, panel, and section builders in ui.ts. Six entries per page leave room for navigation and actions within Discord's 40-component limit. Habit panels use the existing saved habit colors; general screens use the same Mina accent as /today. Each entry has an inline Manage, Open history, Manage reminder, or Preview action. Settings and task-management screens also use native panels so navigation from these commands never attempts to downgrade a V2 message.

History opens the selected date directly, with Previous/Next date and Change date controls. Its checklists paginate six tasks to leave room for date navigation. Manual/count/task saves still use the shared daily handlers and dated requirements.

Normal navigation, forms, confirmations, and recoverable errors edit the source message. Only validated owner controls can update it; expired or cross-account controls receive a separate private error. Existing dropdown controls remain supported until they expire. No new command registration or migration is needed.

Progress previews include a V2 media gallery referencing the uploaded attachment. The deferred PATCH preserves the V2 flag, clears legacy content/embeds, and replaces or removes prior attachments. Theme changes, destination confirmation, live status, and public-send results all stay in the private source message. Public sends still require explicit destination confirmation and retain the shared quotas and replay guards. Optional day/check-in/schedule PNG cards remain separate private snapshots.

components.test.ts checks layout limits and same-message form/navigation flows, plus deferred attachment transport. progress.test.ts covers preview/theme/back navigation, failures, and the existing sharing protections. Live client appearance still requires a Discord desktop/mobile acceptance check.