docs(04-ux-improvements): create phase plan
This commit is contained in:
558
.planning/phases/04-ux-improvements/04-03-PLAN.md
Normal file
558
.planning/phases/04-ux-improvements/04-03-PLAN.md
Normal file
@@ -0,0 +1,558 @@
|
||||
---
|
||||
phase: 04-ux-improvements
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 04-01
|
||||
- 04-02
|
||||
files_modified:
|
||||
- frontend/src/hooks/useUpdates.ts
|
||||
- frontend/src/components/Header.tsx
|
||||
- frontend/src/components/TagSection.tsx
|
||||
- frontend/src/components/ServiceCard.tsx
|
||||
- frontend/src/components/Toast.tsx
|
||||
- frontend/src/App.tsx
|
||||
autonomous: true
|
||||
requirements:
|
||||
- BULK-01
|
||||
- BULK-02
|
||||
- INDIC-01
|
||||
- INDIC-02
|
||||
- INDIC-03
|
||||
- INDIC-04
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "User can dismiss all pending updates with a Dismiss All button in the header area"
|
||||
- "User can dismiss all pending updates within a tag group via a per-section button"
|
||||
- "Dismiss All requires a two-click confirmation before executing"
|
||||
- "A pending-count badge is always visible in the Header"
|
||||
- "The browser tab title shows 'DiunDash (N)' when N > 0 and 'DiunDash' when 0"
|
||||
- "A toast notification appears when new updates arrive during polling"
|
||||
- "Updates received since the user's last visit have a visible amber left border highlight"
|
||||
artifacts:
|
||||
- path: "frontend/src/hooks/useUpdates.ts"
|
||||
provides: "acknowledgeAll, acknowledgeByTag callbacks; newArrivals state; tab title effect"
|
||||
contains: "acknowledgeAll"
|
||||
- path: "frontend/src/components/Header.tsx"
|
||||
provides: "Pending badge, dismiss-all button with confirm"
|
||||
contains: "pendingCount"
|
||||
- path: "frontend/src/components/TagSection.tsx"
|
||||
provides: "Per-group dismiss button"
|
||||
contains: "onAcknowledgeGroup"
|
||||
- path: "frontend/src/components/Toast.tsx"
|
||||
provides: "Custom toast notification component"
|
||||
min_lines: 20
|
||||
- path: "frontend/src/components/ServiceCard.tsx"
|
||||
provides: "New-since-last-visit highlight via isNewSinceLastVisit prop"
|
||||
contains: "isNewSinceLastVisit"
|
||||
- path: "frontend/src/App.tsx"
|
||||
provides: "Wiring: bulk callbacks, toast state, lastVisit ref, tab title, new props"
|
||||
contains: "acknowledgeAll"
|
||||
key_links:
|
||||
- from: "frontend/src/hooks/useUpdates.ts"
|
||||
to: "/api/updates/acknowledge-all"
|
||||
via: "fetch POST in acknowledgeAll callback"
|
||||
pattern: "fetch.*acknowledge-all"
|
||||
- from: "frontend/src/hooks/useUpdates.ts"
|
||||
to: "/api/updates/acknowledge-by-tag"
|
||||
via: "fetch POST in acknowledgeByTag callback"
|
||||
pattern: "fetch.*acknowledge-by-tag"
|
||||
- from: "frontend/src/App.tsx"
|
||||
to: "frontend/src/components/Header.tsx"
|
||||
via: "pendingCount and onDismissAll props"
|
||||
pattern: "pendingCount=|onDismissAll="
|
||||
- from: "frontend/src/App.tsx"
|
||||
to: "frontend/src/components/TagSection.tsx"
|
||||
via: "onAcknowledgeGroup prop"
|
||||
pattern: "onAcknowledgeGroup="
|
||||
- from: "frontend/src/App.tsx"
|
||||
to: "frontend/src/components/ServiceCard.tsx"
|
||||
via: "isNewSinceLastVisit prop passed through TagSection"
|
||||
pattern: "isNewSinceLastVisit"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Wire bulk dismiss UI (frontend) to the backend endpoints from Plan 01, add update indicators (pending badge, tab title, toast, new-since-last-visit highlight).
|
||||
|
||||
Purpose: Completes the UX improvements by giving users bulk actions and visual awareness of new updates.
|
||||
Output: Updated useUpdates hook with bulk callbacks and toast detection, Header with badge + dismiss-all, TagSection with per-group dismiss, Toast component, ServiceCard with highlight.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-ux-improvements/04-CONTEXT.md
|
||||
@.planning/phases/04-ux-improvements/04-RESEARCH.md
|
||||
@.planning/phases/04-ux-improvements/04-01-SUMMARY.md
|
||||
@.planning/phases/04-ux-improvements/04-02-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- From Plan 01: new backend endpoints -->
|
||||
POST /api/updates/acknowledge-all -> {"count": N}
|
||||
POST /api/updates/acknowledge-by-tag (body: {"tag_id": N}) -> {"count": N}
|
||||
|
||||
<!-- From Plan 02: Header already has theme toggle, App.tsx has filter state -->
|
||||
From frontend/src/components/Header.tsx (after Plan 02):
|
||||
```typescript
|
||||
interface HeaderProps {
|
||||
onRefresh: () => void
|
||||
}
|
||||
// Header now has theme toggle button, refresh button
|
||||
```
|
||||
|
||||
From frontend/src/hooks/useUpdates.ts:
|
||||
```typescript
|
||||
export function useUpdates() {
|
||||
// Returns: updates, loading, error, lastRefreshed, secondsUntilRefresh, fetchUpdates, acknowledge, assignTag
|
||||
}
|
||||
```
|
||||
|
||||
From frontend/src/components/TagSection.tsx:
|
||||
```typescript
|
||||
interface TagSectionProps {
|
||||
tag: Tag | null
|
||||
rows: TagSectionRow[]
|
||||
onAcknowledge: (image: string) => void
|
||||
onDeleteTag?: (id: number) => void
|
||||
}
|
||||
```
|
||||
|
||||
From frontend/src/components/ServiceCard.tsx:
|
||||
```typescript
|
||||
interface ServiceCardProps {
|
||||
image: string
|
||||
entry: UpdateEntry
|
||||
onAcknowledge: (image: string) => void
|
||||
}
|
||||
```
|
||||
|
||||
From frontend/src/App.tsx (after Plan 02):
|
||||
```typescript
|
||||
// Has: filteredEntries useMemo, FilterBar, filter state
|
||||
// Uses: useUpdates() destructured for updates, acknowledge, etc.
|
||||
// Stats: pending, acknowledgedCount computed from unfiltered entries
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Extend useUpdates with bulk acknowledge callbacks, toast detection, and tab title effect</name>
|
||||
<files>frontend/src/hooks/useUpdates.ts</files>
|
||||
<read_first>
|
||||
- frontend/src/hooks/useUpdates.ts
|
||||
- frontend/src/types/diun.ts
|
||||
</read_first>
|
||||
<action>
|
||||
1. **Add acknowledgeAll callback** (per D-01, D-02) using optimistic update pattern matching existing `acknowledge`:
|
||||
```typescript
|
||||
const acknowledgeAll = useCallback(async () => {
|
||||
setUpdates(prev =>
|
||||
Object.fromEntries(
|
||||
Object.entries(prev).map(([img, entry]) => [
|
||||
img,
|
||||
entry.acknowledged ? entry : { ...entry, acknowledged: true },
|
||||
])
|
||||
) as UpdatesMap
|
||||
)
|
||||
try {
|
||||
await fetch('/api/updates/acknowledge-all', { method: 'POST' })
|
||||
} catch (e) {
|
||||
console.error('acknowledgeAll failed:', e)
|
||||
fetchUpdates()
|
||||
}
|
||||
}, [fetchUpdates])
|
||||
```
|
||||
|
||||
2. **Add acknowledgeByTag callback** (per D-01, D-02):
|
||||
```typescript
|
||||
const acknowledgeByTag = useCallback(async (tagID: number) => {
|
||||
setUpdates(prev =>
|
||||
Object.fromEntries(
|
||||
Object.entries(prev).map(([img, entry]) => [
|
||||
img,
|
||||
entry.tag?.id === tagID && !entry.acknowledged
|
||||
? { ...entry, acknowledged: true }
|
||||
: entry,
|
||||
])
|
||||
) as UpdatesMap
|
||||
)
|
||||
try {
|
||||
await fetch('/api/updates/acknowledge-by-tag', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tag_id: tagID }),
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('acknowledgeByTag failed:', e)
|
||||
fetchUpdates()
|
||||
}
|
||||
}, [fetchUpdates])
|
||||
```
|
||||
|
||||
3. **Add toast detection** (per D-11): Track previous update keys with a ref. After each successful fetch, compare new keys vs previous. Only fire after initial load (guard: `prevKeysRef.current.size > 0`). State is `newArrivals: string[]`, replaced (not appended) each time.
|
||||
```typescript
|
||||
const prevKeysRef = useRef<Set<string>>(new Set())
|
||||
const [newArrivals, setNewArrivals] = useState<string[]>([])
|
||||
|
||||
// Inside fetchUpdates, after setUpdates(data):
|
||||
const currentKeys = Object.keys(data)
|
||||
const newKeys = currentKeys.filter(k => !prevKeysRef.current.has(k))
|
||||
if (newKeys.length > 0 && prevKeysRef.current.size > 0) {
|
||||
setNewArrivals(newKeys)
|
||||
}
|
||||
prevKeysRef.current = new Set(currentKeys)
|
||||
```
|
||||
|
||||
Add a `clearNewArrivals` callback:
|
||||
```typescript
|
||||
const clearNewArrivals = useCallback(() => setNewArrivals([]), [])
|
||||
```
|
||||
|
||||
4. **Update return value** to include new fields:
|
||||
```typescript
|
||||
return {
|
||||
updates, loading, error, lastRefreshed, secondsUntilRefresh,
|
||||
fetchUpdates, acknowledge, assignTag,
|
||||
acknowledgeAll, acknowledgeByTag,
|
||||
newArrivals, clearNewArrivals,
|
||||
}
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/DiunDashboard/frontend && npx tsc --noEmit</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- useUpdates.ts contains `const acknowledgeAll = useCallback`
|
||||
- useUpdates.ts contains `fetch('/api/updates/acknowledge-all'`
|
||||
- useUpdates.ts contains `const acknowledgeByTag = useCallback`
|
||||
- useUpdates.ts contains `fetch('/api/updates/acknowledge-by-tag'`
|
||||
- useUpdates.ts contains `const prevKeysRef = useRef<Set<string>>`
|
||||
- useUpdates.ts contains `const [newArrivals, setNewArrivals] = useState<string[]>`
|
||||
- useUpdates.ts contains `clearNewArrivals` in the return object
|
||||
- useUpdates.ts return object includes `acknowledgeAll` and `acknowledgeByTag`
|
||||
- `npx tsc --noEmit` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>useUpdates hook returns acknowledgeAll, acknowledgeByTag, newArrivals, and clearNewArrivals; toast detection fires on new images during polling</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Toast component, Header updates, TagSection per-group dismiss, ServiceCard highlight, and App.tsx wiring</name>
|
||||
<files>frontend/src/components/Toast.tsx, frontend/src/components/Header.tsx, frontend/src/components/TagSection.tsx, frontend/src/components/ServiceCard.tsx, frontend/src/App.tsx</files>
|
||||
<read_first>
|
||||
- frontend/src/App.tsx
|
||||
- frontend/src/components/Header.tsx
|
||||
- frontend/src/components/TagSection.tsx
|
||||
- frontend/src/components/ServiceCard.tsx
|
||||
- frontend/src/hooks/useUpdates.ts
|
||||
- frontend/src/types/diun.ts
|
||||
</read_first>
|
||||
<action>
|
||||
1. **Create Toast.tsx** (per D-11): Custom toast component. Auto-dismiss after 5 seconds. Non-stacking (shows latest message only). Props:
|
||||
```typescript
|
||||
interface ToastProps {
|
||||
message: string
|
||||
onDismiss: () => void
|
||||
}
|
||||
```
|
||||
Implementation: fixed position bottom-right (`fixed bottom-4 right-4 z-50`), dark card style, shows message + X dismiss button. Uses `useEffect` with a 5-second `setTimeout` that calls `onDismiss`. Renders `null` if `message` is empty.
|
||||
```tsx
|
||||
export function Toast({ message, onDismiss }: ToastProps) {
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(onDismiss, 5000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [message, onDismiss])
|
||||
|
||||
if (!message) return null
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-50 max-w-sm rounded-lg border border-border bg-card px-4 py-3 shadow-lg flex items-center gap-3">
|
||||
<p className="text-sm flex-1">{message}</p>
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="text-muted-foreground hover:text-foreground text-xs font-medium shrink-0"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
2. **Header.tsx** (per D-03, D-04, D-09): Extend HeaderProps and add pending badge + dismiss-all button.
|
||||
Update the interface:
|
||||
```typescript
|
||||
interface HeaderProps {
|
||||
onRefresh: () => void
|
||||
pendingCount: number
|
||||
onDismissAll: () => void
|
||||
}
|
||||
```
|
||||
Add `Badge` import from `@/components/ui/badge`. Add `CheckCheck` import from `lucide-react`.
|
||||
After "Diun Dashboard" title span, add the pending badge (per D-09):
|
||||
```tsx
|
||||
{pendingCount > 0 && (
|
||||
<Badge variant="secondary" className="text-xs font-bold px-2 py-0.5 bg-amber-500/15 text-amber-500 border-amber-500/25">
|
||||
{pendingCount}
|
||||
</Badge>
|
||||
)}
|
||||
```
|
||||
Add dismiss-all button with two-click confirm pattern (per D-04, matching existing tag delete pattern in TagSection). Add local state `const [confirmDismissAll, setConfirmDismissAll] = useState(false)`. The button:
|
||||
```tsx
|
||||
{pendingCount > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (!confirmDismissAll) { setConfirmDismissAll(true); return }
|
||||
onDismissAll()
|
||||
setConfirmDismissAll(false)
|
||||
}}
|
||||
onBlur={() => setConfirmDismissAll(false)}
|
||||
className={cn(
|
||||
'h-8 px-3 text-xs font-medium',
|
||||
confirmDismissAll
|
||||
? 'text-destructive hover:bg-destructive/10'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<CheckCheck className="h-3.5 w-3.5 mr-1" />
|
||||
{confirmDismissAll ? 'Sure? Dismiss all' : 'Dismiss All'}
|
||||
</Button>
|
||||
)}
|
||||
```
|
||||
Import `useState` from react and `cn` from `@/lib/utils`.
|
||||
|
||||
3. **TagSection.tsx** (per D-03): Add optional `onAcknowledgeGroup` prop. Update interface:
|
||||
```typescript
|
||||
interface TagSectionProps {
|
||||
tag: Tag | null
|
||||
rows: TagSectionRow[]
|
||||
onAcknowledge: (image: string) => void
|
||||
onDeleteTag?: (id: number) => void
|
||||
onAcknowledgeGroup?: (tagId: number) => void
|
||||
}
|
||||
```
|
||||
Add a "Dismiss Group" button in the section header, next to the delete button, only when `tag !== null` and `onAcknowledgeGroup` is provided and at least one row is unacknowledged. Use two-click confirm pattern:
|
||||
```typescript
|
||||
const [confirmDismissGroup, setConfirmDismissGroup] = useState(false)
|
||||
const hasPending = rows.some(r => !r.entry.acknowledged)
|
||||
```
|
||||
Button (placed before the delete button):
|
||||
```tsx
|
||||
{tag && onAcknowledgeGroup && hasPending && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!confirmDismissGroup) { setConfirmDismissGroup(true); return }
|
||||
onAcknowledgeGroup(tag.id)
|
||||
setConfirmDismissGroup(false)
|
||||
}}
|
||||
onBlur={() => setConfirmDismissGroup(false)}
|
||||
className={cn(
|
||||
'flex items-center gap-1 px-2 py-1 rounded text-[11px] font-medium transition-colors',
|
||||
confirmDismissGroup
|
||||
? 'text-destructive hover:bg-destructive/10'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<CheckCheck className="h-3.5 w-3.5" />
|
||||
{confirmDismissGroup ? 'Sure?' : 'Dismiss Group'}
|
||||
</button>
|
||||
)}
|
||||
```
|
||||
Import `CheckCheck` from `lucide-react`.
|
||||
|
||||
4. **ServiceCard.tsx** (per D-12, D-13): Add `isNewSinceLastVisit` prop. Update interface:
|
||||
```typescript
|
||||
interface ServiceCardProps {
|
||||
image: string
|
||||
entry: UpdateEntry
|
||||
onAcknowledge: (image: string) => void
|
||||
isNewSinceLastVisit?: boolean
|
||||
}
|
||||
```
|
||||
Update the outer div's className to include highlight when `isNewSinceLastVisit`:
|
||||
```tsx
|
||||
className={cn(
|
||||
'group p-4 rounded-xl border border-border bg-card hover:border-muted-foreground/30 transition-all flex flex-col justify-between gap-4',
|
||||
isNewSinceLastVisit && 'border-l-4 border-l-amber-500',
|
||||
isDragging && 'opacity-30',
|
||||
)}
|
||||
```
|
||||
|
||||
5. **App.tsx**: Wire everything together.
|
||||
|
||||
a. Destructure new values from useUpdates:
|
||||
```typescript
|
||||
const {
|
||||
updates, loading, error, lastRefreshed, secondsUntilRefresh,
|
||||
fetchUpdates, acknowledge, assignTag,
|
||||
acknowledgeAll, acknowledgeByTag,
|
||||
newArrivals, clearNewArrivals,
|
||||
} = useUpdates()
|
||||
```
|
||||
|
||||
b. Add tab title effect (per D-10):
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
document.title = pending > 0 ? `DiunDash (${pending})` : 'DiunDash'
|
||||
}, [pending])
|
||||
```
|
||||
Add `useEffect` to the React import.
|
||||
|
||||
c. Add last-visit tracking (per D-12):
|
||||
```typescript
|
||||
const lastVisitRef = useRef<string | null>(
|
||||
localStorage.getItem('lastVisitTimestamp')
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => localStorage.setItem('lastVisitTimestamp', new Date().toISOString())
|
||||
window.addEventListener('beforeunload', handler)
|
||||
return () => window.removeEventListener('beforeunload', handler)
|
||||
}, [])
|
||||
```
|
||||
|
||||
d. Compute `isNewSinceLastVisit` per entry when building rows. Create a helper:
|
||||
```typescript
|
||||
function isNewSince(receivedAt: string): boolean {
|
||||
return lastVisitRef.current ? receivedAt > lastVisitRef.current : false
|
||||
}
|
||||
```
|
||||
|
||||
e. Update taggedSections and untaggedRows to include `isNewSinceLastVisit`:
|
||||
```typescript
|
||||
const taggedSections = tags.map(tag => ({
|
||||
tag,
|
||||
rows: filteredEntries
|
||||
.filter(([, e]) => e.tag?.id === tag.id)
|
||||
.map(([image, entry]) => ({ image, entry, isNew: isNewSince(entry.received_at) })),
|
||||
}))
|
||||
const untaggedRows = filteredEntries
|
||||
.filter(([, e]) => !e.tag)
|
||||
.map(([image, entry]) => ({ image, entry, isNew: isNewSince(entry.received_at) }))
|
||||
```
|
||||
|
||||
f. Update TagSectionRow type import in TagSection.tsx or define the `isNew` property. Actually, keep `TagSectionRow` unchanged and pass `isNewSinceLastVisit` through the ServiceCard render. In TagSection.tsx, update `TagSectionRow`:
|
||||
```typescript
|
||||
export interface TagSectionRow {
|
||||
image: string
|
||||
entry: UpdateEntry
|
||||
isNew?: boolean
|
||||
}
|
||||
```
|
||||
And in TagSection's ServiceCard render:
|
||||
```tsx
|
||||
<ServiceCard
|
||||
key={image}
|
||||
image={image}
|
||||
entry={entry}
|
||||
onAcknowledge={onAcknowledge}
|
||||
isNewSinceLastVisit={isNew}
|
||||
/>
|
||||
```
|
||||
Update the destructuring in the `.map()`: `{rows.map(({ image, entry, isNew }) => (`
|
||||
|
||||
g. Update Header props:
|
||||
```tsx
|
||||
<Header onRefresh={fetchUpdates} pendingCount={pending} onDismissAll={acknowledgeAll} />
|
||||
```
|
||||
|
||||
h. Update TagSection props to include `onAcknowledgeGroup`:
|
||||
```tsx
|
||||
<TagSection
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
rows={taggedSections_rows}
|
||||
onAcknowledge={acknowledge}
|
||||
onDeleteTag={deleteTag}
|
||||
onAcknowledgeGroup={acknowledgeByTag}
|
||||
/>
|
||||
```
|
||||
|
||||
i. Add toast rendering and import:
|
||||
```typescript
|
||||
import { Toast } from '@/components/Toast'
|
||||
```
|
||||
Compute toast message from `newArrivals`:
|
||||
```typescript
|
||||
const toastMessage = newArrivals.length > 0
|
||||
? newArrivals.length === 1
|
||||
? `New update: ${newArrivals[0]}`
|
||||
: `${newArrivals.length} new updates arrived`
|
||||
: ''
|
||||
```
|
||||
Add `<Toast message={toastMessage} onDismiss={clearNewArrivals} />` at the end of the root div, before the closing `</div>`.
|
||||
|
||||
j. Import `useEffect` if not already imported (it should be from Plan 02 adding useMemo -- check). The import line should be:
|
||||
```typescript
|
||||
import React, { useState, useRef, useEffect, useMemo } from 'react'
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/DiunDashboard/frontend && npx tsc --noEmit && bun run build</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Toast.tsx exists and exports `Toast` component
|
||||
- Toast.tsx contains `setTimeout(onDismiss, 5000)`
|
||||
- Toast.tsx contains `fixed bottom-4 right-4`
|
||||
- Header.tsx contains `pendingCount` in HeaderProps interface
|
||||
- Header.tsx contains `onDismissAll` in HeaderProps interface
|
||||
- Header.tsx contains `confirmDismissAll` state
|
||||
- Header.tsx contains `Sure? Dismiss all` text for confirm state
|
||||
- Header.tsx contains `Badge` import
|
||||
- TagSection.tsx contains `onAcknowledgeGroup` in TagSectionProps
|
||||
- TagSection.tsx contains `confirmDismissGroup` state
|
||||
- TagSection.tsx contains `Dismiss Group` text
|
||||
- ServiceCard.tsx contains `isNewSinceLastVisit` in ServiceCardProps
|
||||
- ServiceCard.tsx contains `border-l-4 border-l-amber-500`
|
||||
- App.tsx contains `acknowledgeAll` and `acknowledgeByTag` destructured from useUpdates
|
||||
- App.tsx contains `document.title` assignment with `DiunDash`
|
||||
- App.tsx contains `lastVisitTimestamp` in localStorage calls
|
||||
- App.tsx contains `<Toast` JSX element
|
||||
- App.tsx contains `<Header` with `pendingCount=` and `onDismissAll=` props
|
||||
- App.tsx contains `onAcknowledgeGroup=` prop on TagSection
|
||||
- TagSection.tsx TagSectionRow interface contains `isNew`
|
||||
- `bun run build` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>Bulk dismiss buttons work (dismiss-all in header with two-click confirm, dismiss-group in each tag section); pending badge shows in header; tab title reflects count; toast appears for new arrivals; new-since-last-visit items have amber left border highlight</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```bash
|
||||
cd /home/jean-luc-makiola/Development/projects/DiunDashboard/frontend
|
||||
npx tsc --noEmit
|
||||
bun run build
|
||||
# Full stack verification:
|
||||
cd /home/jean-luc-makiola/Development/projects/DiunDashboard
|
||||
go test -v ./pkg/diunwebhook/
|
||||
go build ./...
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Dismiss All button in header triggers POST /api/updates/acknowledge-all
|
||||
- Per-group Dismiss Group button triggers POST /api/updates/acknowledge-by-tag with correct tag_id
|
||||
- Both dismiss buttons use two-click confirmation
|
||||
- Pending count badge visible in header when > 0
|
||||
- Browser tab title shows "DiunDash (N)" or "DiunDash"
|
||||
- Toast appears at bottom-right when polling detects new images
|
||||
- Toast auto-dismisses after 5 seconds
|
||||
- New-since-last-visit updates have amber left border
|
||||
- Frontend builds without TypeScript errors
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-ux-improvements/04-03-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user