# Notifications & Focus redesign — two tabs, nothing unbounded

Approved mock: `home-mocks/notifications.html` (scratchpad, :8642). This spec is the
implementation contract; where mock and spec disagree, the spec wins.

## Goals

1. **Bound the app list.** Recent senders + customized apps render up top; everything else sits
   behind a collapsed, searchable "All apps" expander. Rows expand in place to their controls.
2. **Real per-app rules**: sound on/off, banners-vs-history, urgency override, forget.
3. **A real focus-mode editor**: create/rename/delete/reorder (order IS priority), trigger-kind
   editing for all five kinds, schedules with day pills, chip-based interrupt lists.
4. **Two tabs**: Notifications | Focus, as category tabs like Shell.

Non-goals: lock-screen privacy (hyprlock cannot render notifications; contract-banned),
per-app counters (no data), time-based history retention, merging duplicate app identities
(show the raw id honestly instead).

## Rule shape (pinned — service and UI program against this)

`normalizedAppRule` in `Notifs.qml` grows from `{ enabled }` to:

| Field | Type | Default | Meaning |
|---|---|---|---|
| `enabled` | bool | true | Off = rejected before tracking/history/unread/toast (unchanged) |
| `sound` | bool | true | false = `playBell` skips this app |
| `display` | string | `"banners"` | `"history"` = file in history + unread, no popup, no bell |
| `urgency` | string | `"auto"` | `"low"` / `"critical"` override what the app claims |
| `lastSeenMs` | int | 0 | stamped in `rememberApplication` on every notification |
| `name` | string | "" | display name cached at remember time (resolution stays live-first) |
| `icon` | string | "" | icon cached at remember time (DesktopEntries lookup, else `appIcon`) |

Unknown/stale fields (incl. the old lock-screen pair) keep being dropped on read. Old
`{enabled}`-only blobs stay valid — every new field is optional with the defaults above.
New API: `forgetApp(appId)` deletes the rule key outright.
`effectiveUrgency(notification)` — the app's `urgency` override applied over
`notification.urgency`; consumed by `playBell` (low = silent), the popup timeout choice
(critical duration), the DND breakthrough gate, and `NotificationCard`'s critical edge.

**Critical breakthrough**: new schema key `criticalBreaksThrough` (bool, def **false**, group
`notifications`, label "Critical alerts break through"). Popup gate becomes: show when
`!doNotDisturb || FocusModes.allows(appId) || (Settings.criticalBreaksThrough &&
effectiveUrgency(n) === critical)`.

## FocusModes API additions (pinned)

- `createMode(name)` → new mode `{ id: unique slug, name, enabled: true, triggers: [{kind:
  "manual"}], silence: true, keepAwake: false, allow: [] }`, appended (lowest priority).
- `removeMode(id)`, `renameMode(id, name)` (non-empty, trimmed).
- `moveMode(id, delta)` — reorder; order is priority and the UI says so.
- `setTriggerKind(id, kind, fields)` — replaces the mode's `triggers` with one trigger of the
  new kind. Kind-specific seeds: schedule → `{ start: "22:00", end: "07:00", days: [0..6] }`;
  workspace → `{ id: 1 }`; fullscreen/game/manual → no fields. (Shipped modes each carry one
  trigger; a hand-edited multi-trigger mode collapses to one on first kind change — the editor
  edits `triggers[0]` and that is documented in a comment.)
- **Manual-mode semantics**: investigate how a manual-trigger mode activates today and PRESERVE
  it exactly; if manual modes currently have no activation path besides `enabled`, the header
  toggle keeps meaning `enabled` and the editor's "Turns on: Manually" detail explains that a
  manual mode quiets things whenever it is switched on. Do not invent new activation machinery.
- Existing `setEnabled`/`update`/`reschedule`-style schedule + day editing semantics stay;
  `withinWindow`, single-DND-ownership, and the gaming report-don't-silence rule are
  contract-pinned and untouched.

## Routing (pinned)

`SettingsRoutes` category `notifications` gains tabs:
`[{ page: "notifications", label: "Notifications" }, { page: "focus", label: "Focus" }]`.
New leaf `focus` → new `FocusPage.qml`; `SettingsShell` case + Component. `groupPages`
`"focus"` moves `"notifications"` → `"focus"` (the focusModes entry renders there now).
`GamingPage`'s "Open Focus" action retargets `openSettings("focus")`.
`NotificationCard`'s "Notification settings" jump stays `"notifications"`.

## Notifications tab (NotificationsPage.qml rebuilt)

Lede unchanged. Cards:
1. **Quiet** — Do Not Disturb toggle; "Critical alerts break through" ToggleRow
   (`criticalBreaksThrough`); "Quiet hours" ActionRow whose detail states the Sleep mode's
   live schedule (or "not scheduled" when Sleep lacks/disabled) and whose button opens the
   Focus tab (`openSettings("focus")`).
2. **Banners & history** — the four existing schema sliders (names contract-pinned; critical
   zero renders "Never"); history count + Clear folded into the history row.
3. **Applications** — subtitle per mock. Sections:
   - *Recent*: apps with `lastSeenMs` within 7 days, newest first.
   - *Customized*: any app with a non-default field (and not already in Recent).
   - *All apps (N)*: collapsed expander with an inline search field; alphabetical.
   Rows: cached icon (fallback letter tile), name, subtitle (relative last-seen when known ·
   state summary like "sound off"/"History only", else the raw appId), enabled toggle, chevron.
   Expanded body: Play sound toggle · "Show as" dropdown (Banners & history / History only) ·
   "Urgency" dropdown (App decides / Treat as low / Treat as critical) · "Forget this app"
   ActionRow (detail: "Remove its rule; it returns on its next notification").
4. **Active-mode banner** at top (ok-tinted) when a focus mode is active: "<name> is quieting
   notifications · because <reason> · <allow summary>", button "Open Focus".

## Focus tab (FocusPage.qml, new)

Lede: "Modes quiet this machine on their own terms — first matching mode wins, and the order
below is the priority."
1. **Focus modes** card — accordion (one open at a time): drag grip (reorder = priority; also
   keyboard up/down on the grip), mode glyph, name, summary line ("Turns on <trigger summary> ·
   silences everything except N apps" / "On now — <reason>"), enabled toggle, chevron.
   Expanded: "Turns on" dropdown (five kinds) + kind fields (schedule start/end `TimeOfDayRow`-
   style or validated HH:MM inputs + seven day pills; workspace id picker); "Silence
   notifications" toggle; "May interrupt" chip row (chips with ×, "+ Add app" opens a searchable
   picker over known apps — reuse the rules list's app universe); "Keep the screen awake"
   toggle; Rename + Delete mode buttons. "+ New focus mode" dashed row at the bottom.
2. **Focus sessions** card — default duration segmented chips (25/45/60/90 →
   `focusDurationMinutes`), Caffeine toggle, session status row + Start focus/Show controls
   (existing behaviors move over unchanged).

Reuse existing row widgets and the Displays/Sound phase components (OptionPickerRow, etc.)
before inventing new ones. No continuously repainting animations. All new components get
qmldir lines in the same wave as first reference.

## Search & docs

Hand-written entries (page per target): Do Not Disturb, Quiet hours, Critical alerts break
through, Application notification rules, Forget an app's notifications, Per-app notification
sound → `notifications`; Focus modes detail already schema-indexed (now routes to `focus`),
plus Focus session duration → `focus` if not covered by the workspaces group move. Docs and
launcher commands regenerate after the schema lands (orchestrator's audit pass).

## Contracts (write, do NOT run — cite in the backlog for the next sweep)

- `notification-app-rules-contract`: extend the pinned rule shape to the table above (defaults,
  optional back-compat, stale-field dropping incl. lock-screen pair), pin `forgetApp`, the
  display="history" no-popup-no-bell path, sound=false no-bell, `effectiveUrgency` consumers,
  and the breakthrough gate literal.
- `focus-modes-contract`: pin the new CRUD/reorder/trigger APIs, keep every existing pin
  (conditions-not-alarms, DND ownership, gaming reports, exception list consulted + editable —
  the editable needle moves to FocusPage).
- `settings-pages-contract` (page id list + component list), `settings-jump-contract`
  (GamingPage → focus), `search-routing-contract` expectations, `settings-window-contract` if
  it enumerates tabs.
- Backlog spec gains a Phase 7 section listing all of it.

## Agent ownership (parallel)

- **A — services**: `services/Notifs.qml`, `services/FocusModes.qml`,
  `config/PreferenceSchema.qml` (one new key; comments above braces).
- **B — UI**: `modules/settings/NotificationsPage.qml`, new `modules/settings/FocusPage.qml` +
  new components + `modules/settings/qmldir`, `services/SettingsRoutes.qml`,
  `modules/settings/SettingsShell.qml`, `modules/notifications/NotificationCard.qml`
  (effectiveUrgency), `modules/settings/GamingPage.qml` (Open Focus target).
- **C — periphery**: `services/SettingsSearch.qml`, the contracts above + harness fixtures,
  test-backlog spec, README count line only if the contract count changes.

B programs against the pinned shapes; A must not change them without updating this spec.
