# 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 `