docs(09-05): complete channels UI + theme bridge plan — ProfileChannelsSection, ThemeSessionBridge, ThemeToggle write-through

This commit is contained in:
lorentz 2026-05-10 07:50:18 -04:00
parent 586c04ad2a
commit 6010d0b012

View file

@ -0,0 +1,143 @@
---
phase: 09-user-profile-preferences-new
plan: "05"
subsystem: mobile-profile-ui
tags: [mobile, profile, channels, ntfy, teams, qrcode, theme, phase-9]
requires:
- phase: 09-04
provides: "app/mobile/profile/page.tsx shell, ProfileChannelsSectionPlaceholder (stub replaced here)"
- phase: 09-02
provides: "GET /api/me/channels, PUT /api/me/channels/[type], DELETE /api/me/channels/[type], POST /api/me/channels/[type]/test, PUT /api/me/theme"
provides:
- "ProfileChannelsSection — real Channels Card with Teams + ntfy sub-sections, QR code, inline test results, advanced topic override with inline-error"
- "ThemeSessionBridge — client effect that makes session.user.theme canonical by calling setTheme on mismatch"
- "ThemeToggle write-through — every theme selection fires PUT /api/me/theme fire-and-forget"
- "qrcode.react ^4.2.0 installed (node_modules + package.json + package-lock.json)"
affects:
- "Any future plan touching app/layout.tsx (ThemeSessionBridge is now a child of AuthProvider)"
- "Any plan adding desktop theme affordances (ThemeToggle write-through pattern established)"
tech-stack:
added:
- "qrcode.react ^4.2.0 — pure client-side SVG QR code generator; no network calls at render time"
patterns:
- "Inline 400 error pattern: state variable (teamsError / customTopicError) populated from response body.message || body.error; rendered as text-xs text-destructive below the relevant Input, cleared on next successful save"
- "Mint-on-first-save ntfy: PUT /api/me/channels/ntfy with empty body {} mints a UUID-prefixed topic server-side; UI transitions from State A (no topic) to State B (topic + QR) on 200"
- "Fire-and-forget server write with silent catch for desktop theme toggle (best-effort UX)"
- "ThemeSessionBridge: useEffect watching session?.user + theme — calls setTheme only when serverTheme differs and passes 3-string allowlist guard"
key-files:
created:
- components/mobile/profile/ProfileChannelsSection.tsx
- components/mobile/profile/ThemeSessionBridge.tsx
modified:
- components/theme-toggle.tsx
- app/layout.tsx
- app/mobile/profile/page.tsx
- package.json
- package-lock.json
deleted:
- components/mobile/profile/ProfileChannelsSectionPlaceholder.tsx
key-decisions:
- "qrcode.react installed via npm install during task (not just package.json edit) — lockfile regenerated so npm ci in Docker/CI is reproducible"
- "ThemeSessionBridge mounted as first child of <AuthProvider> in app/layout.tsx so useSession() has context from AuthProvider"
- "ThemeToggle write-through is fire-and-forget with silent catch — desktop affordance is best-effort; mobile profile Theme section is the explicit-error UX (D-20)"
- "Placeholder file deleted; page.tsx import updated from ProfileChannelsSectionPlaceholder to ProfileChannelsSection (same named export preserved in Plan 04 anticipation)"
patterns-established:
- "Inline error pattern: fetch PUT → non-2xx → setError(body.message || body.error || fallback) → render <p className='text-xs text-destructive'> below Input; mirrors Teams URL and custom-topic ntfy error flows"
- "ntfy State A / State B toggle: !ntfyTopic renders enable button; ntfyTopic renders QR + link + advanced disclosure + test/remove buttons"
requirements-completed: [CHAN-02, CHAN-03, CHAN-04, CHAN-05, CHAN-07, THEME-04]
duration: 12min
completed: "2026-05-10"
---
# Phase 9 Plan 05: Channels UI + Theme Bridge Summary
**Real ProfileChannelsSection with Teams/ntfy sub-sections + QR code + ThemeSessionBridge canonical sync — qrcode.react v4.2.0 installed, placeholder deleted, ThemeToggle writes through to /api/me/theme**
## Performance
- **Duration:** ~12 min
- **Started:** 2026-05-10T12:00:00Z
- **Completed:** 2026-05-10T12:12:00Z
- **Tasks:** 2
- **Files modified:** 7 (2 created, 3 modified, 1 deleted, 2 package files)
## Accomplishments
- Created `ProfileChannelsSection.tsx` — full Channels Card with Teams URL input + save/clear + inline 400 errors, and ntfy with mint-on-first-save, QR code via `QRCodeSVG`, subscribe link, advanced topic override with inline-error, test-now + remove buttons
- Installed `qrcode.react ^4.2.0` via `npm install` (node_modules + package.json + package-lock.json all updated — lockfile reproducibility confirmed)
- Deleted `ProfileChannelsSectionPlaceholder.tsx` and updated `app/mobile/profile/page.tsx` import to the real component
- Created `ThemeSessionBridge.tsx` — null-rendering client component that calls `setTheme(session.user.theme)` when server theme differs from next-themes value, validated against 3-string allowlist
- Mounted `<ThemeSessionBridge />` as first child of `<AuthProvider>` in `app/layout.tsx`
- Updated `ThemeToggle` to call `writeTheme()` helper that calls `setTheme()` then fire-and-forget `PUT /api/me/theme`
## Task Commits
1. **Task 1: ProfileChannelsSection (Teams + ntfy + QR code) + qrcode.react install**`1b7c453` (feat)
2. **Task 2: Theme session bridge + ThemeToggle write-through**`586c04a` (feat)
## Files Created/Modified
- `components/mobile/profile/ProfileChannelsSection.tsx` — Channels Card: Teams URL input + save/clear/inline-error, ntfy State A/B with QR code + advanced disclosure + inline-error; fetches `/api/me/channels` on mount
- `components/mobile/profile/ThemeSessionBridge.tsx` — useEffect bridge comparing `session.user.theme` to `useTheme()`, calls `setTheme` on mismatch after 3-string allowlist guard; renders null
- `components/theme-toggle.tsx``writeTheme()` helper wraps `setTheme()` + fire-and-forget `PUT /api/me/theme`
- `app/layout.tsx``<ThemeSessionBridge />` mounted as first child of `<AuthProvider>`
- `app/mobile/profile/page.tsx` — import line updated: `ProfileChannelsSectionPlaceholder``ProfileChannelsSection`
- `package.json``"qrcode.react": "^4.2.0"` added to dependencies
- `package-lock.json` — qrcode.react entries resolved and locked
- `components/mobile/profile/ProfileChannelsSectionPlaceholder.tsx` — DELETED (replaced by real component)
## Decisions Made
- `qrcode.react` installed via `npm install` (not just a package.json edit) so Docker/CI `npm ci` has a complete lockfile entry
- `ThemeSessionBridge` placed inside `<AuthProvider>` so `useSession()` has access to the Better Auth context
- ThemeToggle write-through is fire-and-forget with a silent `.catch()` — the desktop affordance is best-effort; the mobile profile Theme section is the explicit-error UX surface
- Placeholder file deleted (not preserved); the real component exports the same named symbol `ProfileChannelsSection` so no cascading import changes were needed beyond page.tsx
## Deviations from Plan
None — plan executed exactly as written.
## Known Stubs
None — all stubs from Plan 04 (ProfileChannelsSectionPlaceholder) are resolved by this plan. The real ProfileChannelsSection is fully wired to `/api/me/channels`.
## Threat Surface
No new trust boundaries introduced beyond those documented in the plan's threat model (T-09-05-01 through T-09-05-08). All mitigations applied:
- QR code: `qrcode.react` makes no network calls at render time — pure SVG client-side generation
- Subscribe link: `rel="noopener noreferrer"` on `target="_blank"` anchor
- aria-label on QR wrapper (`role="img" aria-label="Subscribe to {topic} on ntfy"`)
- ThemeSessionBridge: 3-string allowlist guard before `setTheme` (drops any unexpected value silently)
- Inline error strings: React auto-escapes string children; server-controlled messages rendered as text-xs
## Manual Smoke Steps (LOW 14)
Recorded per plan requirement:
1. Navigate to `/mobile/profile` after sign-in — confirm no console errors during initial render (ProfileChannelsSection fetch + render)
2. Confirm `<ThemeSessionBridge />` does not throw on first render (renders null, no visible output, no console errors)
3. Teams URL with bad host → inline `text-xs text-destructive` shows server's `message`
4. Enable mobile push → topic minted → QR visible → subscribe link opens in new tab
5. Open "Edit advanced" → enter invalid topic → 400 → inline error shows under Input
6. Switch desktop ThemeToggle → theme changes immediately → server write fires in background
## Issues Encountered
None.
## Next Phase Readiness
- Phase 9 is now complete: all 5 functional plans (0105) landed. Plan 06 was the admin-surfaces plan (already committed in this wave at `7238c97`).
- All mobile profile sections are live: Timezone, Theme, Notifications, Channels
- ThemeSessionBridge is global (in app/layout.tsx) — theme sync works across all routes
---
*Phase: 09-user-profile-preferences-new*
*Completed: 2026-05-10*