feature(persons): Korrespondenz-Überblick dashboard on /persons/[id] #306

Open
opened 2026-04-22 20:19:27 +02:00 by marcel · 0 comments
Owner

Summary

Extend /persons/[id] with a correspondence-at-a-glance block on the right column. Replaces the current CoCorrespondentsList with a richer PersonDashboard that shows stats, activity over the years, direction split, top correspondents, top locations, and a tag cloud. Every element deep-links into /briefwechsel with pre-filled filters — the person page becomes the discovery surface, /briefwechsel stays the reading surface.

Spec

Final HTML spec (scaled wireframes × 3 viewports + impl-ref tables with exact Tailwind classes and pixel values + WCAG contrast verification):

Exploration context (same 5-concept brainstorm as sister issue #305):

What changes visually

  • Right column of /persons/[id] replaces CoCorrespondentsList with a new PersonDashboard block
  • Dark navy header strip with title "Korrespondenz-Überblick" + "↗ Briefwechsel öffnen" CTA
  • Stats strip: 4 cells (Briefe gesamt · ausgehend · eingehend · Jahre) · serif Merriweather Black numbers at 22 px
  • Activity histogram: one bar per year, peak highlighted in navy, others in mint. Click a bar → /briefwechsel?senderId=…&from=YYYY-01-01&to=YYYY-12-31
  • Direction split bar: re-uses DistributionBar.svelte from the thumbnail-rows issue
  • Top correspondents (up to 6) and top locations (up to 5) as ordered lists with proportional bars + counts
  • Tag cloud with size buckets (xl for count ≥ 100, l for 50–99, m for 20–49, regular for 5–19, muted for < 5)
  • Existing sent/received document lists stay below, unchanged

States covered in spec

  • Default (full dataset — 851 letters, 42 years, 87 correspondents)
  • Empty (new person, no letters yet)
  • Sparse (< 10 letters — histogram replaced with year-span line, tag cloud hidden)
  • Loading (skeleton placeholders per section)

Breakdown

Phase 1 — backend aggregation endpoint

  • New GET /api/persons/{id}/dashboard returning PersonDashboardDTO
  • Response schema as per spec (totalCount, outCount, inCount, yearSpan, correspondentCount, activityByYear, peakYear+Count, topCorrespondents, topLocations, topTags)
  • @RequirePermission(Permission.READ_ALL)
  • Server-side cache keyed on (personId, dataVersion); invalidate on Document write hooks
  • Regenerate TypeScript types (npm run generate:api)
  • Tests for empty / sparse / full / cache-invalidation

Phase 2 — frontend components

  • PersonDashboard.svelte — orchestrator
  • StatStrip.svelte — 4-cell stats grid (semantic <dl>)
  • ActivityHistogram.svelte — one bar per year, peak highlight, tooltip + aria-label, click → deep link
  • DistributionBar.svelte — re-used from the thumbnail-rows issue (do not duplicate)
  • TopTileList.svelte — generic ordered list, used for correspondents and locations
  • TagCloud.svelte — frequency-sized chips with size buckets
  • Replace CoCorrespondentsList.svelte usage in /persons/[id]/+page.svelte
  • Remove coCorrespondents derivation from +page.svelte (dashboard owns it server-side)
  • +page.server.ts loads person data + dashboard in parallel

Phase 3 — new query params on /briefwechsel

  • direction=OUT|IN filter
  • location=<slug> filter (case-insensitive match on Document.location)
  • tagId=<uuid> filter
  • Deep links from dashboard tiles wire to these params

Phase 4 — testing

  • axe-playwright tests at 320 / 768 / 1440 in light + dark
  • Visual regression snapshots for all four states × 3 viewports
  • E2E: click a histogram bar → lands on filtered /briefwechsel; click a correspondent → bilateral view

Accessibility contract

All contrast ratios verified in the spec — light and dark mode pass WCAG AA or AAA. Stats as <dl> pairs, histogram is role="img" with descriptive aria-label, top lists are semantic <ol>, tag cloud is <ul> of <li><a>, every interactive element has focus-visible ring, touch targets ≥ 44 px on mobile. prefers-reduced-motion disables bar animations and chip hover lift.

  • Sister issue: #305 (thumbnail rows on /briefwechsel). Both issues share the DistributionBar.svelte component — ship whichever lands first.
  • The new /briefwechsel query params (direction, location, tagId) are prerequisites for the deep links to work end-to-end.
## Summary Extend `/persons/[id]` with a correspondence-at-a-glance block on the right column. Replaces the current `CoCorrespondentsList` with a richer `PersonDashboard` that shows stats, activity over the years, direction split, top correspondents, top locations, and a tag cloud. Every element deep-links into `/briefwechsel` with pre-filled filters — the person page becomes the discovery surface, `/briefwechsel` stays the reading surface. ## Spec Final HTML spec (scaled wireframes × 3 viewports + impl-ref tables with exact Tailwind classes and pixel values + WCAG contrast verification): - [`docs/specs/person-dashboard-spec.html`](../raw/branch/main/docs/specs/person-dashboard-spec.html) Exploration context (same 5-concept brainstorm as sister issue #305): - [`docs/specs/briefwechsel-fill/index.html`](../raw/branch/main/docs/specs/briefwechsel-fill/index.html) — concept 5 evolved into this spec after user feedback ruled out hosting insights on `/briefwechsel` directly. ## What changes visually - Right column of `/persons/[id]` replaces `CoCorrespondentsList` with a new `PersonDashboard` block - Dark navy header strip with title "Korrespondenz-Überblick" + "↗ Briefwechsel öffnen" CTA - Stats strip: 4 cells (Briefe gesamt · ausgehend · eingehend · Jahre) · serif Merriweather Black numbers at 22&nbsp;px - Activity histogram: one bar per year, peak highlighted in navy, others in mint. Click a bar → `/briefwechsel?senderId=…&from=YYYY-01-01&to=YYYY-12-31` - Direction split bar: re-uses `DistributionBar.svelte` from the thumbnail-rows issue - Top correspondents (up to 6) and top locations (up to 5) as ordered lists with proportional bars + counts - Tag cloud with size buckets (xl for count ≥ 100, l for 50–99, m for 20–49, regular for 5–19, muted for &lt; 5) - Existing sent/received document lists stay below, unchanged ## States covered in spec - Default (full dataset — 851 letters, 42 years, 87 correspondents) - Empty (new person, no letters yet) - Sparse (&lt; 10 letters — histogram replaced with year-span line, tag cloud hidden) - Loading (skeleton placeholders per section) ## Breakdown ### Phase 1 — backend aggregation endpoint - [ ] New `GET /api/persons/{id}/dashboard` returning `PersonDashboardDTO` - [ ] Response schema as per spec (totalCount, outCount, inCount, yearSpan, correspondentCount, activityByYear, peakYear+Count, topCorrespondents, topLocations, topTags) - [ ] `@RequirePermission(Permission.READ_ALL)` - [ ] Server-side cache keyed on `(personId, dataVersion)`; invalidate on Document write hooks - [ ] Regenerate TypeScript types (`npm run generate:api`) - [ ] Tests for empty / sparse / full / cache-invalidation ### Phase 2 — frontend components - [ ] `PersonDashboard.svelte` — orchestrator - [ ] `StatStrip.svelte` — 4-cell stats grid (semantic `<dl>`) - [ ] `ActivityHistogram.svelte` — one bar per year, peak highlight, tooltip + aria-label, click → deep link - [ ] `DistributionBar.svelte` — re-used from the thumbnail-rows issue (do not duplicate) - [ ] `TopTileList.svelte` — generic ordered list, used for correspondents and locations - [ ] `TagCloud.svelte` — frequency-sized chips with size buckets - [ ] Replace `CoCorrespondentsList.svelte` usage in `/persons/[id]/+page.svelte` - [ ] Remove `coCorrespondents` derivation from `+page.svelte` (dashboard owns it server-side) - [ ] `+page.server.ts` loads person data + dashboard in parallel ### Phase 3 — new query params on /briefwechsel - [ ] `direction=OUT|IN` filter - [ ] `location=<slug>` filter (case-insensitive match on `Document.location`) - [ ] `tagId=<uuid>` filter - [ ] Deep links from dashboard tiles wire to these params ### Phase 4 — testing - [ ] axe-playwright tests at 320 / 768 / 1440 in light + dark - [ ] Visual regression snapshots for all four states × 3 viewports - [ ] E2E: click a histogram bar → lands on filtered `/briefwechsel`; click a correspondent → bilateral view ## Accessibility contract All contrast ratios verified in the spec — light and dark mode pass WCAG AA or AAA. Stats as `<dl>` pairs, histogram is `role="img"` with descriptive aria-label, top lists are semantic `<ol>`, tag cloud is `<ul>` of `<li><a>`, every interactive element has `focus-visible` ring, touch targets ≥ 44&nbsp;px on mobile. `prefers-reduced-motion` disables bar animations and chip hover lift. ## Related - Sister issue: #305 (thumbnail rows on /briefwechsel). Both issues share the `DistributionBar.svelte` component — ship whichever lands first. - The new `/briefwechsel` query params (`direction`, `location`, `tagId`) are prerequisites for the deep links to work end-to-end.
marcel added the featurepersonui labels 2026-04-22 20:19:36 +02:00
Sign in to join this conversation.
No Label feature person ui
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#306