From 19d9724c9cc8d737253aa5873801dda35c3a2ab3 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 24 Mar 2026 09:34:29 +0100 Subject: [PATCH] docs(phase-04): research ux-improvements phase --- .../phases/04-ux-improvements/04-RESEARCH.md | 627 ++++++++++++++++++ 1 file changed, 627 insertions(+) create mode 100644 .planning/phases/04-ux-improvements/04-RESEARCH.md diff --git a/.planning/phases/04-ux-improvements/04-RESEARCH.md b/.planning/phases/04-ux-improvements/04-RESEARCH.md new file mode 100644 index 0000000..38c0d6a --- /dev/null +++ b/.planning/phases/04-ux-improvements/04-RESEARCH.md @@ -0,0 +1,627 @@ +# Phase 4: UX Improvements - Research + +**Researched:** 2026-03-24 +**Domain:** React SPA (search/filter, toast, theme, drag UX) + Go HTTP handlers (bulk acknowledge endpoints) +**Confidence:** HIGH — all findings are based on direct inspection of the live codebase. No third-party library unknowns; every feature maps to patterns already present in the project. + +--- + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +**Bulk dismiss (BULK-01, BULK-02)** +- D-01: Add two new Store methods: `AcknowledgeAll() (count int, err error)` and `AcknowledgeByTag(tagID int) (count int, err error)` — consistent with existing `AcknowledgeUpdate(image)` pattern +- D-02: Two new API endpoints: `POST /api/updates/acknowledge-all` and `POST /api/updates/acknowledge-by-tag` (with `tag_id` in body) — returning the count of dismissed items +- D-03: UI placement: "Dismiss All" button in the header/stats area; "Dismiss Group" button in each TagSection header next to the existing delete button +- D-04: Confirmation: modal/dialog confirmation for dismiss-all (high-impact action); inline confirm pattern (matching existing tag delete) for per-group dismiss + +**Search and filter (SRCH-01 through SRCH-04)** +- D-05: Client-side filtering only — all data is already in memory from polling, no new API endpoints needed +- D-06: Filter bar placed above the sections list, below the stats row +- D-07: Controls: text search input (filters by image name), status dropdown (all/pending/acknowledged), tag dropdown (all/specific tag/untagged), sort dropdown (date/name/registry) +- D-08: Filters do not persist across page reloads — reset on each visit + +**New-update indicators (INDIC-01 through INDIC-04)** +- D-09: Pending update badge/counter displayed in the Header component next to the "Diun Dashboard" title — always visible +- D-10: Browser tab title reflects pending count: `"DiunDash (N)"` when N > 0, `"DiunDash"` when zero +- D-11: Toast notification when new updates arrive during polling — auto-dismiss after 5 seconds with manual dismiss button; non-stacking (latest update replaces previous toast) +- D-12: "New since last visit" detection via localStorage timestamp — store `lastVisitTimestamp` on page unload; updates with `received_at` after that timestamp get a visual highlight +- D-13: Highlight style: subtle left border accent (`border-l-4 border-amber-500`) on ServiceCard for new-since-last-visit items + +**Accessibility and theme (A11Y-01, A11Y-02)** +- D-14: Light/dark theme toggle placed in the Header bar next to the refresh button — icon button (sun/moon) +- D-15: Theme preference persisted in localStorage; on first visit, respects `prefers-color-scheme` media query; removes the hardcoded `classList.add('dark')` from `main.tsx` +- D-16: Drag handle on ServiceCard always visible at reduced opacity (`opacity-40`), full opacity on hover — removes the current `opacity-0 group-hover:opacity-100` pattern + +### Claude's Discretion +- Toast component implementation (custom or shadcn/ui Sonner) +- Exact filter bar layout and responsive breakpoints +- Animation/transition details for theme switching +- Whether to show a count in the per-group dismiss button (e.g., "Dismiss 3") +- Sort order default (most recent first vs alphabetical) + +### Deferred Ideas (OUT OF SCOPE) +None — discussion stayed within phase scope. + + +--- + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| BULK-01 | User can acknowledge all pending updates at once with a single action | New `AcknowledgeAll` Store method + `POST /api/updates/acknowledge-all` handler; optimistic update in useUpdates follows existing acknowledge pattern | +| BULK-02 | User can acknowledge all pending updates within a specific tag/group | New `AcknowledgeByTag` Store method + `POST /api/updates/acknowledge-by-tag` handler; TagSection receives `onAcknowledgeGroup` callback prop | +| SRCH-01 | User can search updates by image name (text search) | Client-side filter on `entries` array in App.tsx; filter state via useState; case-insensitive substring match on image key | +| SRCH-02 | User can filter updates by status (pending vs acknowledged) | Client-side filter on `entry.acknowledged` boolean already present in UpdateEntry type | +| SRCH-03 | User can filter updates by tag/group | Client-side filter on `entry.tag?.id` against tag dropdown value; "untagged" = null tag | +| SRCH-04 | User can sort updates by date, image name, or registry | Client-side sort on `entries` array before grouping; `received_at` (string ISO 8601 sortable), image key (string), registry extracted by existing `getRegistry` helper in ServiceCard | +| INDIC-01 | Dashboard shows a badge/counter of pending (unacknowledged) updates | `pending` count already computed in App.tsx; Badge component already exists; wire into Header props | +| INDIC-02 | Browser tab title includes pending update count | `document.title` side effect in useUpdates or App.tsx useEffect watching pending count | +| INDIC-03 | In-page toast notification appears when new updates arrive during polling | Detect new images in fetchUpdates by comparing prev vs new keys; toast state in useUpdates hook; custom toast component or Radix-based | +| INDIC-04 | Updates that arrived since the user's last visit are visually highlighted | localStorage `lastVisitTimestamp` written on `beforeunload`; read at mount; compare `entry.received_at` ISO string; add `isNewSinceLastVisit` boolean to derived state | +| A11Y-01 | Light/dark theme toggle with system preference detection | Tailwind `darkMode: ['class']` already configured; toggle adds/removes `dark` class; localStorage + `prefers-color-scheme` media query init replaces hardcoded `classList.add('dark')` in main.tsx | +| A11Y-02 | Drag handle for tag reordering is always visible (not hover-only) | Change `opacity-0 group-hover:opacity-100` to `opacity-40 hover:opacity-100` on the grip button in ServiceCard.tsx | + + +--- + +## Summary + +Phase 4 adds UX features across the entire stack. The backend requires two new SQL operations (`UPDATE ... WHERE acknowledged_at IS NULL` for all rows, and the same filtered by tag join) and two new HTTP handlers following the exact pattern already used for `DismissHandler`. No schema changes, no migrations, no new tables. + +The frontend work is pure React/TypeScript. All features are enabled by the existing stack: client-side filter/sort, toast via a lightweight component, theme via the already-configured Tailwind `darkMode: ['class']` strategy, localStorage for persistence of theme preference and last-visit timestamp, and a one-line opacity change for the drag handle. No new npm packages are strictly required. The one discretionary choice is the toast implementation: a small custom component avoids a new dependency; `sonner` (shadcn/ui's recommended toast) is an option if polish justifies the dependency. + +**Primary recommendation:** Implement everything with existing dependencies. Use a custom toast component (30 lines of Tailwind CSS) rather than installing sonner. Use native `` is unacceptable for design reasons | + +**Version verification:** The above new packages are NOT currently in `package.json`. Before adding any, run `bun add ` to pull the latest version from the registry. Do not assume training-data version numbers. + +**Recommendation:** Use native HTML `