Phase 6: Discord and desktop notifications as channel plugins, settings as Schemas #11
Loading…
Reference in a new issue
No description provided.
Delete branch "refactor/phase-6-channels"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 6 of the DDD/CQRS refactor: Discord and desktop notifications become plugins, settings become Schemas, and plugins get their own settings and secrets. Stacked on #10.
What changes
Plugins can be configured. A plugin declares its settings and secrets as Schemas on
@Plugin({ settings, secrets })and reads them withctx.settings(schema)/ctx.secrets(schema). They are saved per plugin (plugin_settings.settings,plugin_secrets), and secrets are sealed with AES-256-GCM using a key file in the data dir (secrets.key, 0600). The plugin's card has a Configure form drawn from the Schemas. Secrets never come back to the browser; only their last four characters are shown as a hint. Saving restarts the plugin, which replacesrestartDiscord.Channels are a contribution kind. A channel is
class X extends Channel(orPromiseChannel) with@ChannelType. It receivesChannelEvents (question asked or answered, plan questions open, notification created) in order from its own inbox, and can keep a long-lived connection (run), report a status and offer a Test button; both show on the plugin's card. A channel works with Kanbot only through its context:board: reads of tasks, questions, plans, reviews, notifications, attachments and standups, plus answering, commenting and attaching. Each needs a permission (board:read,board:answer,board:comment), and writes are recorded as originplugin:<id>.store: a key-value store, replacingdiscord_messages(migrated).webSocket: checked against thenetworkpermission.host.notify.Discord is
plugins/discord. The 1,022-lineserver/integrations/discord.tsis now about 30 files, all under the size limit. 93 characterisation tests were written against the old bot first and pass unchanged against the plugin; they use a fake Discord REST API and gateway./api/discord/*is gone.The OS notification is
plugins/host-desktop, a channel usinghost:notify. The desktop setting and the desktop app's own notifications behave as before.Settings are Schemas (
packages/core/src/settings). Each section has a Schema with defaults, and reading is one decode, so adding a field is one edit (it used to need four). Changing settings is a command (ChangeSettings) that records an event. That event is projected to the live messages screens already handle, and consumers follow up on it (the standup timer, the scheduler). The app reads settings through one sharedSettingsRPC query, which retires therefreshNuxtData('app-settings')workaround.PUT /api/settingsis gone;GETstays for the Electron shell. Discord left AppSettings: an old install's Discord settings are adopted into the plugin once, at boot, andenabled: falseturns the plugin off.Checks
LEGACY_API_ROUTES: 8 → 5. Lint baseline: 21 → 20 files.secrets.keyis-rw-------;Plugins.formreturns only the hintWXYZ;Settings.updatesurvives a restart;Behaviour changes, on purpose
KANBOT_DISCORD_APIandKANBOT_DISCORD_GATEWAYare no longer read. A configurable address would be somewhere to send the bot's token.Not in this PR
🤖 Generated with Claude Code
A setting had to be added in four places (the defaults and the per-field merge in getSettings, the zod schema and setSetting in PUT /api/settings, the app's defaultSettings) or it silently dropped. Now each section is a Schema in the Settings context (packages/core/src/settings), its defaults in SETTINGS_DEFAULTS beside it (plain data, so the app shows them without loading Effect), and reading is decoding the stored rows with it (readSettings): a field or section never stored reads back as its default, a row that does not decode as its section's default. The legacy fixes are decode steps: groomingModel becomes planningModel, this machine's launchers and the KANBOT_* numbers apply only where nothing was stored, and the columns workflows provide are added by the server's shim. getSettings, getSettingsRow, publicSettings and setSetting stay as that synchronous shim, so the legacy readers and the characterisation tests are unchanged; the old merge is kept in test/settings as the reference every stored shape must still read back as. Changing settings is the ChangeSettings command over the new Settings RPC group (get, update a patch of whole sections): checked by the Schemas, text trimmed and the board's own columns kept as PUT did, and one SettingsChanged (LocaleChanged for the language) per section that really changed. They are projected to the messages screens already handle (board.changed, launchers.changed, runner.changed, sync.changed, locale.changed) plus settings.changed, which the app's single settings query listens for; a consumer restarts the standup timer and lets the scheduler start what a higher limit allows. PUT /api/settings goes; GET stays for Electron. The app reads one shared copy (useAppSettings on useRpcQuery) and writes through useSettingsCommands, whose answer replaces that copy, so the refreshNuxtData('app-settings') workaround and defaultSettings() go. The tour finishes through the same command. Discord is a plugin now: its section, types and form leave the settings (the Integrations section goes; the tour and the menu point at Plugins). At boot, before the plugins start, a stored `discord` row becomes the Discord plugin's settings and secret (and an off bot an off plugin), then the row is dropped, so it happens once. The legacy bot reads the plugin's config until its plugin replaces it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>