# Native Notifications and System Tray Design **Status:** approved for implementation on 2026-07-14 ## Goal Deliver Todo reminders as native notifications on Windows and Linux, and keep Verstak running in the system tray when its main window is closed. The feature must work in the portable Windows archive, Debian package, and AppImage. ## Scope - The desktop core owns notification delivery, scheduling, and tray lifetime. - `verstak.todo` owns the Todo-specific reminder policy and text. - No new official notifications plugin is introduced. A dynamic plugin cannot call Wails directly, so it cannot be the native-notification transport. - macOS is out of scope for this alpha. The interfaces remain platform-neutral where that costs nothing. ## Tray behavior On Windows and Linux, a tray icon is registered before the Wails event loop starts. Its menu contains exactly two actions: 1. **Show Verstak** — shows and focuses the existing main window. 2. **Quit** — exits the process deliberately. Closing the main window with its window-manager close control hides the window and keeps the process, plugins, local browser receiver, and reminder scheduler alive. It does not terminate the application. The quit action temporarily allows the close lifecycle to finish and then exits normally. The app has a single-instance lock. If a user launches the executable while an instance is hidden in the tray, the existing instance shows its window instead of creating a second process. The implementation uses `github.com/getlantern/systray` through a small `internal/shell/tray` adapter. It uses `Register`, rather than its blocking `Run`, so Wails remains the owner of the GUI event loop. The current project icon is embedded in the executable; Windows receives the existing `.ico`, and Linux receives the existing PNG. ## Notification capability and permission The core registers the capability: ``` verstak/core/notifications/v1 ``` Plugins that use it must both require that capability and declare the `notifications.schedule` permission. The plugin-host API exposes only two operations within the calling plugin namespace: ``` api.notifications.replace(items) api.notifications.clear() ``` `replace` is an atomic desired-state replacement, not an append operation. An item contains a plugin-local stable `id`, an ISO-8601 UTC `dueAt`, a title, and a body. The core supplies the plugin ID and rejects calls from disabled, missing-permission, or undeclared-capability plugins. It also validates empty IDs, duplicate IDs, invalid timestamps, and unsafe oversized text. No plugin can send arbitrary immediate native notifications or address another plugin's schedules in this alpha. ## Scheduler and persistence `internal/core/notifications` persists one canonical schedule file at: ``` /.verstak/notifications/schedules.json ``` Each record contains `{pluginId, id, dueAt, title, body, sentForDueAt}`. The composite `(pluginId, id)` is unique. Replacing an item with the same due time preserves `sentForDueAt`; changing `dueAt` clears it. Replacing a plugin's list removes its stale records. This provides deterministic cancellation for completed, deleted, and rescheduled Todos. The manager starts after Wails reaches `OnDomReady`, initializes Wails native notifications, and evaluates the persisted schedule immediately and then at least every 30 seconds. A sender is injected behind an interface for unit tests. After a successful delivery, the manager atomically records `sentForDueAt`. A delivery error leaves the schedule pending and is logged for a later retry. An expired record that has not been sent is delivered once after the next app start. A record already sent for its current due time is never sent again. Completely quitting Verstak stops the scheduler: no separate daemon or OS background service is added. Hiding the window in the tray does **not** stop it. The core calls `CleanupNotifications` during shutdown, including on Linux where it releases the D-Bus connection. ## Todo behavior `verstak.todo` adds the core notifications capability to `requires` and adds the `notifications.schedule` permission. After every successful Todo storage write, it derives the complete desired reminder list: - include only open Todos with a valid `reminderAt`; - use the Todo ID as the stable notification ID; - convert local `datetime-local` input to an ISO-8601 UTC instant; - use the Todo title in the notification body and locale-aware reminder text; - call `api.notifications.replace` with the full list. The same replacement runs after loading persisted Todos, so a transient schedule-write failure repairs itself next time the Todo view is opened. A schedule API failure does not roll back Todo data; the UI reports the failure instead. The existing in-view overdue/reminder badge remains useful context and is not removed. ## Packaging `getlantern/systray` requires CGO. The Windows release build already uses `x86_64-w64-mingw32-gcc`; the Windows packaging tests must compile it with the tray dependency included. Linux build instructions add `libayatana-appindicator3-dev`. The Debian package declares the corresponding runtime dependency `libayatana-appindicator3-1`. The existing AppImage packager traverses `ldd` for the desktop executable and copies non-glibc runtime libraries. Its verification is extended to prove that the appindicator library is present in the AppDir when the tray implementation is compiled in. ## Test and manual verification Automated tests cover: - schedule replacement, cancellation, rescheduling, persistence, one-time overdue delivery, failed-send retry, and permission/capability rejection; - Todo desired-list derivation and calls after create/edit/status/delete; - close policy: ordinary close hides, explicit quit permits shutdown; - tray controller action wiring and second-instance window reveal; - Linux/Windows build scripts and package dependency expectations. Manual smoke tests are required because neither unit tests nor Playwright can assert a real desktop notification area or OS toast: 1. On Linux and Windows, start Verstak, close its window, use the tray menu to reveal it, and use **Quit** to terminate it. 2. Set a Todo reminder for a near future time, hide the window in the tray, and observe one native notification. 3. Quit before a future reminder, relaunch after it expires, and observe one overdue notification with no duplicate on the next scheduler scan.