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.