- Status: done
- Date: 2026-05-29
- Specs touched: docs/specs/NOTIFICATIONS.md
Completed the brain-side emit path for the two notification follow-ups that the bell already knew how to render (slice notification-web-ui.md): the member transparency variant and the "all clear" on resolve (NOTIFICATIONS.md # Member transparency variant, # Clears). Backend-only — no web-ui change.
- Member transparency variant (
internal/notify/notify.go). On a raise of a box-blocking storage drive issue (data-drive-missing,data-drive-wrong,data-drive-readonly), the notifier now emits — alongside the admin actionable notification — an info-only, non-actionable notice broadcast to members: summary "Saving is paused", body pointing at the admin, no fix link. Gated by a per-rulememberTransparencyflag on the curated allowlist, not by the issue'sblocks_writesflag:canary-mismatch/mergerfs-assembly-failedalso block writes but stay admin-only (they are System/state plumbing, not the member-legible "your saving is paused" condition — matching the spec's allowlist table exactly). - New
membersaudience (internal/notify,internal/store,internal/api). AddedAudienceMembers(+VariantTransparency) — a class broadcast that is the mirror ofadmins: visible to every non-admin, to no admin (admins get the actionable copy). The store's singlenotificationVisibilityClausechokepoint now routes members to'members'rows; the API's per-id visibility guard (404-not-403) accepts a member acting on amembersrow. Per-recipient read/dismiss rides the existingnotification_readsjoin unchanged. No schema migration —audiencewas already freeTEXT. - "All clear" on resolve (
internal/notify/notify.go). On a health-issue clear the notifier now (a) resolves the problem notification (unchanged) and (b) emits a briefinfo"all clear" — to admins always (per-ruleclearSummary), and to members for transparency issues. The all-clear is a distinct row keyed<problem>:cleared, so the original stays on the timeline marked resolved while the fresh all-clear re-pings the bell. - Flap handling. A raise retracts (resolves) the paired
:clearedall-clear, so a clear→raise flap doesn't leave a false "reconnected" notice next to the fresh problem. Idempotent no-op on a first raise.
Dedup-key layout per issue (up to four coalescing slots): health:<id>[:inst] (admin problem), …:member (member notice), …:cleared / …:member:cleared (the all-clears).
Tests: internal/notify gained focused coverage for the member notice, the non-transparency exclusion, both all-clears, the admin-only all-clear, and stale-all-clear retraction; internal/store and internal/api gained members-audience scoping tests (member sees it, admin doesn't, member can act, admin gets 404). Pre-existing notify/cmd-brain tests whose counts shifted were updated. Full suite green with -race (excluding the PAM-cgo host-agent/pamverifier packages, which don't build in the sandbox).
NOTIFICATIONS.md# Member transparency variant — realized: info-only, non-actionable, broadcast to members; actionable copy to admins; curated allowlist gate; clears with the issue.NOTIFICATIONS.md# Clears — realized: resolve-and-emit-all-clear; original kept resolved, not deleted ("the timeline stays honest").NOTIFICATIONS.md# Routing —membersadded as the transparency broadcast audience (reconciled the model'saudienceenum and the Recipients list in the spec, same change).- Reuses the locked per-recipient read-state model (
notification_reads) and the coalescing/dedup invariant from slices health-notifications.md/notification-read-surface.md without changing them.
- Severity flattening. The member notice and both all-clears are always
info, regardless of the source issue's severity — deliberate (the spec treats the variant as informational; the member's experience is identical whatever the drive fault). The admin problem notification still copies the issue's severity verbatim. - Shared member copy. All transparency issues share one "Saving is paused" message — correct while the allowlist is the three drive issues (all pause saving). When
disk-fulllands (its detector is deferred) the copy may want an upload-specific variant; revisit then. disk-fullnot yet wired. The spec lists it as the canonical transparency example, but it has no detector yet (not inhealthRules); it inherits this behavior the moment its rule is added.- No producer change.
cmd/brain'semitHealthNotificationsis unchanged — the new behavior is entirely insideNotifier.HealthRaised/HealthCleared.
- Per-user, per-category mute (
NOTIFICATIONS.md# Configuration) — the last v1 notification-config item; needs a small prefs table + a filter in the list/count queries and a settings UI. - Retention / pruning for
notifications+notification_reads(NEXT.md# Observability) — capped count/age; the all-clear adds rows that should age out. - Out of this slice's scope but in the same family: SMART/
disk-fulldetectors (would light up the storage transparency path), update-outcome and security-audit notification sources (NOTIFICATIONS.md# The notification list).