Skip to content

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-0022 chrome trilogy (0024 settings tabs · 0025 notifications bell · 0026 help 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: 0022 chrome/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/update for 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 in api.js). "Culled 3,412 frames — 1,140 keepers."
  • Export finishedexportSet resolves. "Exported 214 frames to export/."
  • AI review finishedreview resolves (esp. useful when triggered then navigated away). "Review ready: {frame}."
  • Object removal finishedremoveObject resolves. "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/update applies 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 when unread > 0; a small count is optional. Purely a glance target when idle.
  • The popover anchors under the bell (same scrim/elevation vocabulary as the 0022 menus): a reverse-chronological list of notification rows — icon (reuse Icon.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 call add(...) 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).
  • at timestamps come from the client at add time — 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). sessionStorage for 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 (0001 stateless 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/available returning {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 sessionStorage so a tab reload keeps recent items? Recommendation: in-memory MVP, add sessionStorage only 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 injected checkUpdate seam (WithUpdateCheck) wired only for a packaged app (like doUpdate). The command-layer checkForUpdate resolves the latest release tag via GTB's SelfUpdater.GetLatestVersionString and compares with version.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.svelte in the chrome header (between the theme toggle and the gear): a gold unread dot, a popover (icon · title · detail · relative time · Clear) with Esc/click-out; opening marks all read. An actionable row runs its action (the update notice deep-links to Settings → Updates via openSettings("updates"), 0024). Icon.svelte gained bell.
  • Wiring. Completions raise a notification even off-screen (R-NOTIF-2): cull (CullSession.startCull done/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-lint 0, svelte-check 0/0. Screenshotted live in both themes.