0025 — Notifications bell (chrome popover)¶
- Status: IMPLEMENTED (2026-07-16) — open questions RESOLVED: Q1 → yes, add a
check-only
GET /api/v1/update/available(installing stays explicit); Q2/Q3/Q4 take the recommended defaults (in-memory MVP · bell-only, no toasts · launch + slow interval, update-available only). See §7 for the shipped shape. Second of the post-0022chrome trilogy (0024settings tabs ·0025notifications bell ·0026help panel). - Why now: the studio runs work that finishes off-screen — a cull over 4,000 frames, an export, an AI review, an object removal, a model download — and the only feedback is on the surface that launched it. If Hailey starts a cull and wanders to another shoot, nothing tells her it finished. And an available update is buried in Settings. A bell in the chrome, top-right by the gear, that lights up when there's something to know, collects these into one glanceable place.
- Depends on:
0022chrome/header (where the bell lives), the existing job surfaces (cull SSE stream, export/review/remove request-response),0024(deep-link into Settings → Update tab),pkg/version/updatefor the update-available check. - Non-goals: not OS-level/desktop notifications (localhost tab only); not a persistent cross-session inbox or audit log; not a chat/activity feed. No new long-running job types — this observes the jobs that already exist.
1. What raises a notification¶
Two classes, by origin:
Client-observed (job completion). The SPA already sees these finish — the notification is raised where the promise/stream resolves, no backend needed:
- Cull finished — the cull SSE stream emits
done(progress infra already exists inapi.js). "Culled 3,412 frames — 1,140 keepers." - Export finished —
exportSetresolves. "Exported 214 frames toexport/." - AI review finished —
reviewresolves (esp. useful when triggered then navigated away). "Review ready: {frame}." - Object removal finished —
removeObjectresolves. "Erased region on {frame}." - Job failed — any of the above rejects → an error-toned notification with the message (so a failure that happened off-screen isn't silently lost).
Server-signalled (ambient). Needs a backend signal the client polls or streams:
- Update available — a check-only probe (does not apply) → "krites {new
version} is available." Clicking deep-links to
0024's Update & shutdown tab. (This needs a check-that-doesn't-install endpoint — see Q1; today's/api/v1/updateapplies immediately.) - Model download finished (if
0024/providers surface one) — an ONNX model or runtime finished downloading in the background.
2. Shape¶
- The bell sits in
App.svelte's header, left of the gear. An unread dot (accent, not verdict-coloured —R-REDESIGN-2) shows whenunread > 0; a small count is optional. Purely a glance target when idle. - The popover anchors under the bell (same scrim/elevation vocabulary as the
0022menus): a reverse-chronological list of notification rows — icon (reuseIcon.svelte), title, one-line detail, relative time — with a Mark all read / Clear control. An actionable row (update available, "view keepers") carries a click target that navigates/deep-links. Empty state: "No notifications." - Opening the popover marks its items read (clears the dot). Read state and the list are client-side (see §3).
3. Delivery & persistence (the honest architecture)¶
- A small client store (
notifications.svelte.js, runes) is the source of truth:add({kind,title,detail,at,action}),unread,markAllRead(),clear(). Job surfaces calladd(...)on completion. This covers every client-observed event with zero backend and works even when the launching surface has been navigated away from (the SPA is still alive). attimestamps come from the client ataddtime — no server clock needed; rendered as relative ("2 min ago").- Ambient events (update-available, background downloads) come from a light
poll on an interval (e.g. version check every N minutes) that calls
add(...)when state flips, de-duplness by kind+version so it fires once. - Persistence scope: in-memory for the session is the MVP (krites is a
single-user localhost tool; a reload starts clean).
sessionStoragefor the list is a cheap upgrade if we want it to survive an in-tab reload. No server-side store — consistent with "krites keeps no state of its own" (0001).
4. Requirements¶
- R-NOTIF-1 The bell shows an unread indicator iff there is ≥1 unread
notification; opening the popover clears unread; the indicator uses an accent,
never a verdict colour (
R-REDESIGN-2). - R-NOTIF-2 Cull / export / review / removal completions (and failures) raise a notification even when the user has navigated away from the launching surface.
- R-NOTIF-3 An "update available" notification appears only from a check that
does not install the update; acting on it deep-links to Settings → Update
(
0024), where installing stays the explicit, user-initiated action (R-*update-is-never-automatic, preserved). - R-NOTIF-4 The store is client-side; no server-persisted notification state,
no new durable files in the shoot or config (
0001stateless guarantee). - R-NOTIF-5 The popover is keyboard-operable and dismissible (
Esc, click- out); both themes WCAG AA.
5. Testing¶
- Unit: the store — add/unread/markAllRead/clear, de-dup of the update kind.
- Playwright: run a (faked/fast) cull → bell lights → open popover → row present, dot clears; a rejected job → error-toned row; empty state.
- The update-available path tested against a stubbed check endpoint (available / up-to-date), asserting it never triggers an install.
6. Open questions¶
- Q1 — update-check endpoint → RESOLVED: yes. Add a check-only
GET /api/v1/update/availablereturning{available, latest}without installing; the notification fires from this, and installing stays the explicit action in Settings → Updates (0024). Reuse GTB's updater check half. - Q2 — persistence. In-memory-per-session (MVP) or
sessionStorageso a tab reload keeps recent items? Recommendation: in-memory MVP, addsessionStorageonly if it feels lossy in use. - Q3 — toasts. Also surface a completion as a transient toast (auto-dismiss) at the moment it happens, in addition to landing in the bell? Or bell-only to stay quiet? Recommendation: bell-only first; add toasts for failures only if silent failures prove confusing.
- Q4 — poll interval / scope. What cadence for the update check (e.g. once at launch + every 6 h), and do we poll for anything else ambient? Recommendation: launch + a slow interval; update-available only to start.
7. As built (IMPLEMENTED 2026-07-16)¶
- Backend.
GET /api/v1/update/available→{available, latest}, backed by an injectedcheckUpdateseam (WithUpdateCheck) wired only for a packaged app (likedoUpdate). The command-layercheckForUpdateresolves the latest release tag via GTB'sSelfUpdater.GetLatestVersionStringand compares withversion.CompareVersions— it never installs. No checker wired (dev/CLI) or a transient check failure →200 {available:false}, so the poll stays quiet and a failed poll never spams. Go tests pin all four cases + that the probe never calls the installer. - Store.
lib/notifications.svelte.js— a runes-class singleton:add,addOnce(key,…)(dedupes an unread key so the update notice fires once),markAllRead,clear,unread; capped at 50, newest-first, in-memory. Six vitest unit tests. - Bell.
Bell.sveltein the chrome header (between the theme toggle and the gear): a gold unread dot, a popover (icon · title · detail · relative time · Clear) withEsc/click-out; opening marks all read. An actionable row runs its action (the update notice deep-links to Settings → Updates viaopenSettings("updates"),0024).Icon.sveltegainedbell. - Wiring. Completions raise a notification even off-screen (R-NOTIF-2): cull
(
CullSession.startCulldone/error), export (doExport), AI review (ReviewPanel.doReview), object removal (Loupe.acceptRemove) — successes and failures (failures error-toned). The update poll runs at launch + every 6 h. - Verified. 26 Playwright (2 new: available→lit bell + deep-link row + dot
clears; no-update→quiet + empty state), 77 vitest, Go studio tests,
golangci-lint0, svelte-check 0/0. Screenshotted live in both themes.