Three free axe checks light up (light / system-dark / manual-dark) without
further code changes — they run the existing parameterized spec against
/chronik.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Alle anzeigen" link at the bottom of the notification dropdown now
points to /chronik with the new "Zur Chronik →" label key, matching the
unified activity page introduced in #285.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- "Alle anzeigen" link now goes to /chronik (was /documents — the dead-end
bug called out in #285).
- Rollup rows (count > 1) render a primary-colored count badge plus a
compound timestamp line: "14. Apr. · 14:02–14:32" (en-dash U+2013).
- Singleton rows render the existing "14. Apr. 2026" date line.
- BLOCK_REVIEWED now has a verb mapping (re-using the annotation verb until
the spec pins a distinct copy).
- Three new spec cases: rollup count badge + en-dash range, no badge on
singletons, /chronik link assertion.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The app is pre-production — no 301 redirect, the old route and its tests
are removed outright. Profile page's "Benachrichtigungsverlauf ansehen"
link now points to /chronik.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
page.server.ts loads /api/dashboard/activity (limit=40) and unread
/api/notifications in parallel via Promise.allSettled so a dashboard-activity
failure still renders the Für-dich box. Form actions ?/dismiss and ?/mark-all
back the Dismiss and "Alle gelesen" controls with CSRF-safe SvelteKit
endpoints.
+page.svelte composes all six chronik components:
- ChronikFuerDichBox at the top, seeded from the SSR unread set on first
render and switching to the live SSE singleton once notifications arrive;
- ChronikFilterPills below, wired to URL via goto(?filter=…) with
replaceState so the browser history stays clean across filter changes;
- ChronikTimeline for the day-bucketed feed, filtered client-side per pill
(alle / fuer-dich / hochgeladen / transkription / kommentare);
- ChronikEmptyState for first-run vs filter-empty states;
- ChronikErrorCard on activity load failure.
"Mehr laden" pagination keeps focus on the button after load (via tick() +
$state-bound ref), renders 3 static skeleton rows with aria-busy, and
announces "{count} weitere Einträge geladen" through a polite aria-live
region. Inbox-zero in the Für-dich box links to /chronik?filter=fuer-dich.
Co-located page.server.spec.ts covers load(): limit=40, unread=read:false,
filter parsing with "alle" fallback, activity-fulfilled-but-not-ok surfaces
loadError, plus the dismiss and mark-all actions (success + missing-id
branch). 8 tests green.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All under src/lib/components/chronik/:
- ChronikRow.svelte — single orchestrator for four variants (comment / for-you /
rollup / simple), discriminated via $derived. Outer <a> wraps avatar + body +
time; document title is a styled <span> (no nested anchors). Rollup shows
count badge + en-dash time range; for-you gets accent left border + @ marker
hidden below sm:.
- ChronikTimeline.svelte — buckets items by day using bucketByDay() and renders
Heute/Gestern/Diese Woche/Älter section headers with <span> trailing rule.
- ChronikFuerDichBox.svelte — unread mentions card with inbox-zero variant,
per-row Dismiss button (prevents bubbling, calls onMarkRead), aria-live count
badge, and a .fade-in class gated by prefers-reduced-motion.
- ChronikFilterPills.svelte — role=radiogroup with 5 pills, ArrowLeft/Right
keyboard navigation wrapping across the group, single tabstop via dynamic
tabindex.
- ChronikEmptyState.svelte — three variants (first-run / filter-empty /
inbox-zero) sharing a centered-column layout.
- ChronikErrorCard.svelte — warning card with retry button, optional custom
message override.
Verbs map to chronik_singleton_* / chronik_rollup_* per AuditKind so no ICU
pluralization is needed. Comment preview is a TODO placeholder (currently the
document title) pending a backend preview DTO follow-up.
All 40 unit tests green. No type-check or lint errors in these files.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- docs/adr/003-chronik-unified-activity-feed.md: records the session-rollup
decision (LAG + 120-min gap), the dedupe deletion, the single-endpoint
composition, and the German-URL convention.
- frontend/messages/{de,en,es}.json: adds chronik_* keys for page title,
Für-dich box, filter pills, day headers, singleton/rollup verb variants
per kind, empty states, error card, Mehr-laden pagination, and the Bell
footer link retarget.
No pluralization via ICU match — separate singleton/rollup keys per verb,
per the Felix discussion (comment #3573).
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Left over from the hook→singleton refactor — NotificationDropdown still
imported from the deleted $lib/hooks path.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure function bucketByDay(date, now?, locale?) returns one of
'today'|'yesterday'|'thisWeek'|'older' so ChronikTimeline can
bucket activity rows by relative day without pulling a date
library.
Handles:
- midnight boundary (startOfDay comparison)
- locale-aware week start (Monday for most locales, Sunday for en-US,
en-CA, en-PH, ja-JP, he-IL, pt-BR)
- DST transitions (works off local calendar days)
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the per-component createNotificationStream() factory with a shared
$lib/stores/notifications.svelte.ts singleton. Ref-counted init()/destroy()
ensures one EventSource per tab no matter how many consumers mount
simultaneously.
Motivation: the /chronik "Für dich" box (#285) needs the same live-arrival
stream that NotificationBell already consumes. Two factories would open two
SSE connections per tab — this refactor avoids the silent regression before
it ships.
- New: src/lib/stores/notifications.svelte.ts (module state, refcount)
- New: src/lib/stores/notifications.svelte.spec.ts (proves single EventSource
across multiple consumers + ref-counted teardown)
- Deleted: src/lib/hooks/useNotificationStream.svelte.ts (factory)
- Deleted: src/lib/hooks/__tests__/useNotificationStream.svelte.test.ts
- NotificationBell now imports the singleton
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds count (required) and happenedAtUntil (optional) to the TypeScript DTO so
Chronik + DashboardActivityFeed can consume rollup rows type-safely.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- V49__add_audit_log_rollup_index.sql: partial covering index on
(actor_id, document_id, kind, happened_at DESC) filtered by the 6 rollup
kinds. Matches the WHERE clause of findRolledUpActivityFeed exactly so the
session-grouping window scan is index-backed.
- DashboardController: clamp limit to 40 (was 20). Chronik requests up to 40
activity items per page; dashboard side-rail still passes 7.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The method no longer deduplicates by hour-trunc — it performs session-style
rollup via LAG()+120-min gap. Rename aligns the public name with the
behavior.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites the activity feed query to group consecutive events on the same
(actor, document, kind) into sessions separated by >120 min gaps. A session
becomes one row with count = events-in-session and happenedAtUntil = last
event timestamp. Singletons keep count=1 / happenedAtUntil=null.
Algorithm: LAG() to get the previous event's timestamp in the same partition,
mark a new session when gap > 7200s, then SUM() over an unbounded preceding
window yields a running session_id. Aggregation groups by session_id.
COMMENT_ADDED and MENTION_CREATED always start a new session — these kinds
never roll up so each event stays its own row.
Also adds BLOCK_REVIEWED to the eligible-kinds WHERE clause (Chronik spec §02)
so reviewed blocks appear in the activity feed.
Five new integration tests cover combine-within-2h, split-at-boundary,
no-hard-cap-on-long-session, never-rolls-up-comments/mentions, and the
count/happenedAtUntil contract on both singletons and rollups.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prepares the activity feed data shape for session-style rollup (#285). Adds two
new fields that carry null-operation defaults for the existing hour-truncated
dedupe query:
- count: int (required) — always 1 for singleton rows
- happenedAtUntil: OffsetDateTime (nullable) — end-of-session timestamp for
future rollup rows; null for singletons
No behavioral change yet — the rollup SQL rewrite lands in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Design spec for a dashboard widget that surfaces documents
needing metadata after batch upload. Placed between Resume
strip and MissionControlStrip rather than as a 4th strip
column (strip at visual capacity; batch reality makes count
tiles useless for seniors).
Covers responsive behavior at 320/768/1440, row anatomy with
72/64px touch targets, state matrix (empty/loading/error/
after-upload), full a11y contract, dark-mode verification
notes, and an impl-ref table with exact Tailwind classes.
Refs #296
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ProgressRing renders SVG + percentage label as a flex column (~52px
total). With items-center, the contributor circles aligned to the middle
of the full block, placing them 8px below the ring center. Changed to
items-start on the container and wrapped ContributorStack in h-9 (36px =
SVG height) flex items-center so both circles center at the same 18px.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When sort=SENDER, documents group under the sender's display name card.
When sort=RECEIVER, a document appears under each receiver's card
(with multi-receiver duplication). Falls back to i18n labels for unknown
sender/receiver. Passes sort prop from /documents page to DocumentList.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spec replaces /notifications with a unified /chronik page that merges ambient
archive activity (6 of 8 AuditKinds) and personal mentions/replies. Covers 11
content states across 320/768/1440px viewports, dark mode parity, row anatomy
close-ups, interaction states, WCAG contrast verification, and implementation
notes (routing, API calls, rollup logic, Svelte component structure, i18n keys).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolved 9 conflicts:
- AuditLogQueryRepository/Service: keep HEAD (findRecentContributorsForDocuments)
- ContributorStack: merge main key fix + text-[10px] with HEAD safeColor + aria
- DashboardResumeStrip: merge main text-[10px] with HEAD safeColor
- +page.server/svelte + tests: keep HEAD (pure dashboard, no isDashboard)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The delete button used type=button + requestSubmit() to trigger the form,
which did not reliably fire SvelteKit's enhance submit listener. Replaced
with a type=submit button and an async enhance callback that guards with
the confirm dialog and calls cancel() on rejection.
Also clears the unsaved-changes dirty flag before the redirect so
beforeNavigate doesn't silently block the post-delete navigation.
Closes#277
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TranscriptionQueueService was importing ActivityActorDTO and AuditLogQueryService
from the dashboard package, creating an inverted dependency (service → dashboard).
Moving these to the audit package where AuditLog lives gives both DashboardService
and TranscriptionQueueService the correct dependency direction (→ audit).
Moved to audit:
- ActivityActorDTO, ActivityFeedRow, ContributorRow, PulseStatsRow (projections)
- AuditLogQueryRepository, AuditLogQueryService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace (actor.name ?? actor.initials + i) with (actor.initials + '-' + actor.color)
to fix operator-precedence bug that made keys order-dependent when name is null
- Add role="img" + aria-label={actor.name ?? actor.initials} so screen readers
and touch users can access contributor names
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
findMostRecentDocumentIdByActor only matched TEXT_SAVED events, so documents
where the user drew annotation bounding boxes (but typed no transcription text)
were invisible to the hero resume card. Extending the IN clause to include
ANNOTATION_CREATED lets annotation-only work surface in the card (0% progress,
no excerpt — the correct state before transcription begins).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AuditService.logAfterCommit() called writeLog() inline inside the afterCommit()
callback. At that point Spring's transaction synchronizations are still active on
the thread, so SimpleJpaRepository.save() throws IllegalStateException which the
catch block silently swallowed — leaving audit_log permanently empty.
Fix: submit writeLog() to auditExecutor so it runs on a fresh thread with no active
synchronization context. Also switch auditExecutor from CallerRunsPolicy to AbortPolicy
to prevent the bug from silently recurring when the queue fills under load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- +layout.svelte: Upload button in header (authenticated users only)
- +page.server.ts: call /api/dashboard/resume, /pulse, /activity;
remove deprecated /api/documents/incomplete and /recent-activity
- +page.svelte: 2-col grid layout (main + 320px sidebar), greeting,
DashboardFamilyPulse + DashboardActivityFeed in sidebar
- DashboardResumeStrip: refactored to use server data (resumeDoc prop),
SVG thumbnail, progress bar with aria-*, empty state, CTA
- DashboardFamilyPulse: new component — weekly stats from audit_log
- DashboardActivityFeed: new component — activity feed with "für dich" badge
- Update specs for new data shapes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Greeting, resume card, mission control, family pulse, activity feed,
audit action verbs, and dropzone keys for the Issue #271 dashboard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>