Skip to content

feat(web): F12 — stale + blind-spot push notifications (PRD-007/RFC-006)#37

Merged
explosivebit merged 2 commits into
developfrom
feature/notify-f12
May 6, 2026
Merged

feat(web): F12 — stale + blind-spot push notifications (PRD-007/RFC-006)#37
explosivebit merged 2 commits into
developfrom
feature/notify-f12

Conversation

@explosivebit
Copy link
Copy Markdown
Contributor

Summary

Implements PRD-007 + RFC-006. Browser Notification API (no Service Worker) fires when the 10s /api/health poll detects workspace decay. Plus EVID-014 closes F11 and activates PRD-006 + RFC-005.

Detection

  • entities/health/lib/notify.svelte.ts exports pure helpers: snapshotFromHealth / detectBreaches / notificationsSupported / notificationPermission / requestPermission / fire.
  • 3 breach categories: new blind_spot, stale_count delta, orphan_count delta.
  • Throttle ≥ 60s per category with injectable now() for tests.
  • 9 unit tests cover snapshot extraction, all 3 breach categories, permission branches (granted/denied/missing), throttle window.

Permission UX

  • HealthBar 🔔 / 🔕 toggle. Active state when granted + opted in.
  • First click → requestPermission(); grant flips toggle on.
  • Denied → button disabled with explanatory tooltip.
  • Hidden when 'Notification' in window === false (Firefox no-API users, SSR).

Click → focus

  • Notification's onclick writes to a notifyBus.pendingFocus $state singleton.
  • HomePage's $effect reads it and calls selectNode({id}). Cross-scope state without prop drill.

Privacy & a11y

  • No body content in payload (NFR-002): only id + title for blind_spots, delta count for stale/orphan.
  • silent: true on the Notification (no audio).
  • Hidden aria-live="polite" .sr-only mirror in HealthBar — screen readers announce regardless of OS notification visibility.

Settings

  • notify: boolean field added; persisted via existing loadSettings / saveSettings localStorage flow.

Vitest config

  • pool: 'threads' (was default forks). Default per-file child process spawning blew through macOS kern.maxprocperuid at 8 test files; threads share heap, no spawn() per file. Fix landed during build.

EVID-014 / activations

  • EVID-014 (Tactical, supports / CL3 / test) closes F11 acceptance.
  • PRD-006 + RFC-005 transition draft → active.

Verify

  • npx svelte-check — 0/0/434 files.
  • npm test — 62/62 (53 baseline + 9 new).
  • node scripts/smoke.mjs — PASS.

Test plan

  • CI smoke matrix (3-OS × Node 22) green.
  • svelte-check clean on PR.
  • vitest 62/62.
  • Manual: toggle Notify on; trigger a stale by forgeplan mark-stale <id> in the workspace; observe browser notification; click it; tab focuses + artifact panel opens.

Refs: PRD-006 RFC-005 EVID-014 PRD-007 RFC-006

🤖 Generated with Claude Code

Implements PRD-007 + RFC-006. Browser Notification API (no Service
Worker) fires on workspace health changes detected by the 10s
/api/health poll diff.

Detection (entities/health/lib/notify.svelte.ts):
- detectBreaches(prev, next) returns 3 categories:
  - new blind_spot — id appeared in next.blind_spots that wasn't in prev
  - stale — next.stale_count > prev.staleCount
  - orphan — next.orphan_count > prev.orphanCount
- All pure functions; 9 vitest unit tests cover snapshot extraction,
  every breach category, permission branches (granted/denied/missing),
  throttle window with injectable now() clock.

Permission UX (HealthBar.svelte):
- 🔔/🔕 toggle button with active state when granted+opted-in.
- First click → requestPermission(); on grant, sets notify=true.
- denied → button disabled with tooltip "Re-enable in browser site
  settings".
- Feature-detect: hidden when Notification API absent (NFR-003).
- Hidden aria-live="polite" .sr-only mirror for screen-readers.

Wiring (HomePage.svelte):
- One $effect watches healthPoller.state.data — diffs against
  prevHealthSnapshot, runs detectBreaches, fires per-breach
  notification with onClick → focusArtifact(id).
- Second $effect watches notifyBus.pendingFocus — when set,
  selectNode({id}) and clears. Bus pattern lets notification's
  onclick (foreign closure) reach Svelte 5 state without prop drill.

Settings (settings.ts):
- notify: boolean added to PersistedSettings + ResolvedSettings.
- Default false. Loaded/saved through existing localStorage flow.

Throttle:
- ≥ 60s per category (FR-007).
- silent: true on Notification ctor (no audio).
- tag: `forgeplan.${kind}` collapses repeats in the OS notification
  centre.
- No body content in payload — only id + title for blind_spots,
  count delta for stale/orphan (NFR-002 privacy).

Vitest config:
- pool: 'threads' (was default 'forks'). Default per-file child
  process spawning blew through macOS kern.maxprocperuid at 8
  test files; threads share heap, no spawn() per file.

Verify:
- svelte-check 0/0/434.
- npm test 62/62 (53 baseline + 9 new).
- node scripts/smoke.mjs PASS.

Refs: PRD-007 RFC-006
EVID-014 (Tactical, supports / CL3 / test) — DOM-verified F11
acceptance: PR #36 merged to develop, 13 new vitest tests, 3-OS smoke
matrix green. SC-1..SC-6, SC-8, SC-9 all pass; SC-7 (bundle delta)
tracked but pending per-bundle measurement.

Linked: informs PRD-006, informs RFC-005, builds-on EVID-013.

Status transitions:
- PRD-006: draft → active
- RFC-005: draft → active

These activations ride along with the F12 PR for atomic delivery.
F11 code already shipped in PR #36; this commit only updates artifact
metadata and adds the evidence pack.

Refs: PRD-006 RFC-005 EVID-014
@explosivebit explosivebit merged commit a2cca66 into develop May 6, 2026
3 checks passed
@explosivebit explosivebit deleted the feature/notify-f12 branch May 6, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant