Commit Graph

44 Commits

Author SHA1 Message Date
Marcel
68cb6e9b76 feat(frontend): add mentionSerializer — pure serialize/deserialize for Tiptap ↔ block storage
Converts between the stored format (text + PersonMention sidecar) and Tiptap
ProseMirror JSONContent. Round-trip invariant: serialize(deserialize(t,s)).text === t.
Handles multi-paragraph text (split/join on \n), sidecar deduplication, and
backward compat with old-format full-name sidecar entries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 15:03:58 +02:00
Marcel
060db69108 refactor(person-mention): extract computeHoverCardPosition into testable util
Three reviewer concerns land here:
- Felix #2: magic numbers 0.7 and 300 belong in named constants
- Sara #6: the position function had 4 branches and 2 thresholds with zero tests
- Leonie FINDING-05: at 320px viewport the flip-left could push the card
  past the right edge — needed a viewport clamp

Move the function to src/lib/utils/hoverCardPosition.ts as a pure
(rect, viewport) → {top, left} mapping, with named exports CARD_WIDTH_PX,
CARD_HEIGHT_PX, CARD_GAP_PX, BOTTOM_BAND_RATIO, RIGHT_FLIP_THRESHOLD_PX.
Add a viewport clamp so left + CARD_WIDTH never exceeds the right edge.

Ten unit tests cover default placement, flip-up (both triggers), flip-left,
flip-right-edge clamp, and scroll offset. TranscriptionReadView passes the
current window viewport in on each call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 08:53:29 +02:00
Marcel
1842e23c81 refactor(person-mention): centralise PERSON_MENTION_SELECTOR constant
Markus flagged that 'a.person-mention' is a magic string repeated four times
in TranscriptionReadView, plus the CSS rule, plus tests. Extract into a single
exported constant so the renderer template, the delegated event handlers,
and the consumer-side selectors all import the same value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 08:51:25 +02:00
Marcel
26519d029a feat(person-mention): reject non-UUID personIds at the renderer boundary
Nora's CWE-601 (Open Redirect) defense-in-depth concern: today the backend
emits UUIDs, but renderTranscriptionBody concatenates personId straight into
an href. If a future "external person" feature ever flows a non-UUID through
the sidecar, the renderer would happily emit `<a href="javascript:…">`.

Add a strict UUID regex check before substituting. Non-UUID entries fall
through unchanged so the @-trigger remains as plain text — no silent data
loss, no clickable redirect.

Three new failing→passing tests cover javascript: scheme, absolute URL, and
the positive case (well-formed UUID still renders). Existing tests that used
synthetic IDs ("p-short", "p-first", etc.) updated to real UUIDs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 08:50:28 +02:00
Marcel
488d4384a1 refactor(person-mention): brand renderer return types as SafeHtml
Markus, Felix, and Nora independently flagged the {@html …} boundary as a
distributed-knowledge security risk: today renderBody and renderTranscriptionBody
return string, so the next refactor that does {@html block.text} (instead of
{@html renderBlockHtml(block)}) is one typo away from a stored-XSS regression.

Introduce a SafeHtml brand type (string with a phantom __brand) returned by
both renderers and by renderBlockHtml in TranscriptionReadView. Compile-time
enforcement of the escape invariant — costs zero runtime, makes the contract
auditable in one file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 08:48:26 +02:00
Marcel
eb6e21f032 feat(person-mention): renderTranscriptionBody for safe read-mode HTML
Replaces every @DisplayName in a transcription block's text with an anchor
link to /persons/{personId}, sourced from the mentionedPersons sidecar.
The @ prefix is stripped from the rendered link text per spec — it is an
editor affordance, not part of the historical text.

Stored-XSS hardening: HTML-escapes block text, displayName, and personId
before injection. Word-boundary lookahead avoids prefix collisions
(@Hans vs @HansMüller). Longest-displayName-first + first-sidecar-wins
make rendering deterministic for the OQ-1 collision case (#5339).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 08:12:52 +02:00
Marcel
b4b46a0a79 test(person-mention): boundary cases for whitespace + newline triggers
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m17s
CI / OCR Service Tests (pull_request) Successful in 32s
CI / Backend Unit Tests (pull_request) Failing after 3m6s
CI / Unit & Component Tests (push) Failing after 3m24s
CI / OCR Service Tests (push) Failing after 28s
CI / Backend Unit Tests (push) Failing after 3m43s
Tester #5506 nit pile:
- '@Aug @Bert' with cursor past the second @ — confirm the most
  recent @ wins (this is the canonical case for typing two mentions
  separated by a space).
- '@Aug\\nfoo' with cursor exactly at the newline (index 4) — the
  query still reads 'Aug' because the newline is past the cursor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 01:21:38 +02:00
Marcel
ba73387d50 refactor(transcription): extract saveBlockWithConflictRetry into a util
Tester #5506 §2 + Markus #5504 §2: the 409 orchestration was inline in
+page.svelte and untested. Extract into a pure module that takes the
fetch function as a dependency, so the full happy path / 409 path / 500
path / refetch-fails path / UUID-guard path can be unit-tested with
mock Responses. The route file now reads as 12 lines: call the helper,
on conflict apply the merged snapshot to local state, re-throw.

BlockConflictResolvedError now carries the merged block on its
`merged` property so callers don't have to redo the refetch.

6 new unit tests cover every branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 01:20:49 +02:00
Marcel
362a84dde9 fix(escapeHtml): cover apostrophe to harden single-quoted attribute use
Sina #5505 action item: escapeHtml escaped the four common entities but
not the apostrophe. Today every consumer uses double-quoted attributes,
but a future renderer change to single quotes would silently open a
stored-XSS hole. Cheaper to fix now, with a regression test.

Also pin the idempotence-by-composition property: a second call
re-escapes the & introduced by the first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 01:09:13 +02:00
Marcel
fd3a44d10c refactor(transcription): typed BlockConflictResolvedError instead of prose throw
Felix #3: the 409 path was throwing a human-prose Error which read like
an i18n string that escaped translation. Replace with a named class
carrying code='CONFLICT_RESOLVED' so callers can branch on intent and
future error reporters can map the structured code instead of grepping
strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 01:05:47 +02:00
Marcel
64a61f705c feat(transcription): handle 409 rename-mid-edit conflict on block save (B12b)
When PersonService renames a person while a transcriber is editing a
block that mentions them, the block-save endpoint returns 409 (carrying
the new ErrorCode.PERSON_RENAME_CONFLICT from PR-A). saveBlock now:

1. Refetches the latest server snapshot of the block.
2. Calls mergeBlockOnConflict to combine: server's mentionedPersons
   (post-rename displayNames win) + transcriber's unsaved text + any
   local-only mentions added since the last save.
3. Updates the local block state with the merged result.
4. Re-throws so the autosave indicator surfaces the conflict and the
   pending payload is preserved for retry (B12).

The merge logic is a pure function so it can be unit-tested in
isolation and reused for any future conflict-resolution scenarios.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:35:27 +02:00
Marcel
b3ce15f0dd feat(mention): add detectPersonMention with multi-word query support
Comment mentions stop at a space; person mentions must accept spaces
because historical display names are commonly multi-word.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:03:20 +02:00
Marcel
c7013f4902 refactor(mention): extract shared escapeHtml helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:02:03 +02:00
Marcel
f76af8c678 feat(bulk-upload): add bulkTitleFromFilename utility
Converts a raw filename into a human-readable title candidate by
stripping the extension and replacing underscore/hyphen runs with spaces.
Reuses the existing stripExtension() helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
7f40c54b3f feat(utils): add buildCommentHref helper for comment deep-links
Single source of truth for constructing /documents/:id?commentId=…
(&annotationId=…) URLs. Used by the notification bell, the chronik
"Für dich" sidebar, and the chronik main feed so the three surfaces
can no longer diverge.

Refs #300.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:15:48 +02:00
Marcel
b07f9efa9c fix(document-detail): force edit panel on notification deep-link
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m39s
CI / OCR Service Tests (push) Successful in 30s
CI / Backend Unit Tests (push) Failing after 2m46s
Comments only render inside TranscriptionEditView, so a deep-link
into a document with existing reviewed transcriptions landed the
user in read mode with no comment element in the DOM — the scroll
target silently missed.

scrollToCommentFromQuery now takes a setPanelMode callback and calls
it with 'edit' whenever both query params are present. The page's
own transcribe-mode $effect checks a skipInitialPanelMode flag the
deep-link flow sets, so its default-panel-mode logic doesn't race
against the explicit override.

Two new helper tests pin the contract: panel mode is forced to
'edit' both when transcribe mode is off (entering fresh) and when
it is already on (same-page notification click).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:22:38 +02:00
Marcel
251eb9c3fc feat(frontend): add scrollToCommentFromQuery helper for notification deep-link
Pure function that reads commentId + annotationId from the page URL,
enters transcribe mode if needed, activates the block's annotation,
scrolls the target comment into view, focuses it for screen readers,
fires the existing annotation flash, and strips the params via the
injected callback.

All side effects go through callbacks so the helper is unit-testable
without mounting the page or a DOM (only scrollIntoView/focus are
called on the injected element). Eight tests cover both absent params,
happy path, transcribe-mode activation, missing DOM target, reduced
motion, flash trigger, and URL strip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:29:52 +02:00
Marcel
56161f9a49 feat(utils): add date-buckets helper for Chronik day grouping
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>
2026-04-20 20:38:10 +02:00
Marcel
d31ea12086 feat(upload): validate MIME type and size on file replace in DocumentEditLayout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 23:36:31 +02:00
Marcel
8ed66ae82f feat(frontend): add countRequiredFilled utility with all 8 field-combination tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 23:36:31 +02:00
Marcel
06eb1cada8 refactor(#240): deduplicate formatDate, use generated types, always-visible strip
- Add formatMCDate() to $lib/utils/date.ts (locale-aware, medium format);
  remove duplicated inline formatDate() from all three column components
- Replace local TranscriptionQueueItemDTO/TranscriptionWeeklyStatsDTO type
  declarations with imports from $lib/generated/api across all four components
- Add dashed empty states to SegmentationColumn and TranscriptionColumn
  (ReadyColumn already had one)
- Remove outer {#if} from MissionControlStrip so the section is always
  visible — each column owns its own empty state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 12:28:20 +02:00
Marcel
bc3fec11a9 refactor(comments): extract CommentMessage component from CommentThread (#198)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 14:23:25 +02:00
Marcel
8be876492c refactor(date): consolidate formatDate in date.ts with optional format param
Add format?: 'short'|'long' (default 'long') to date.ts formatDate and
remove the duplicate from personFormat.ts. Update DocumentTopBar to
import from date.ts directly. Move the formatDate tests from
personFormat.spec to date.spec.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 13:10:44 +02:00
Marcel
76d6f234b4 refactor(personFormat): replace getInitials(Person) with getInitials(name: string)
Unify the initials-extraction logic: the new string-based getInitials()
splits on whitespace, takes the first char of the first and last word
uppercased — matching the pattern that was already inlined in
CommentThread. Update PersonChip, DocumentMetadataDrawer, and
CommentThread to use the shared function.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 13:07:23 +02:00
Marcel
655a2003cb refactor(time): extract relativeTime into shared time.ts utility
Move relativeTime from notifications.ts (Intl.RelativeTimeFormat) to a
new time.ts that uses the Paraglide comment_time_* message keys — the
same logic that was already in CommentThread's timeAgo(). Remove the
duplicate timeAgo() from CommentThread and re-export relativeTime from
notifications.ts for backwards compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 13:02:49 +02:00
Marcel
a9aa1ec924 feat(search): add groupDocuments utility with unit tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 23:36:35 +02:00
Marcel
f11d8a38ed feat(frontend): replace all name concatenation with displayName
- Add displayName default method to PersonSummaryDTO
- Update native SQL queries to include title, person_type columns
- Add getInitials() utility to personFormat.ts
- Update abbreviateName/abbreviateCompact for nullable firstName
- Replace firstName+lastName concatenation with displayName in all
  person-displaying components and server load files
- Regenerate API types with displayName on Person and PersonSummaryDTO

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 12:22:30 +02:00
Marcel
3279342ea7 feat(util): add splitByMarkers for [unleserlich] and [...] text splitting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:00:23 +02:00
Marcel
2c0748d60e feat(utils): add debounce utility with full test coverage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:23:14 +02:00
Marcel
27254fb0ac feat(utils): add personFormat utility module with 6 pure functions (TDD)
abbreviateName, formatXsMeta, personAvatarColor (djb2), formatDate,
statusDotClass, statusLabel — 27 tests all green

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 22:39:44 +02:00
Marcel
6d61297182 fix(tests): fix 27 failing frontend unit tests
Six categories of breakage:

1. date.ts — add formatGermanDateInput(raw: string): string as a pure
   function covering both digit-stream auto-dot and manual-dot-with-padding
   modes. Refactor handleGermanDateInput to delegate to it. Fixes 16 failures
   in date.spec.ts where the function was imported but didn't exist.

2. Admin layout specs (groups/tags/users) — $effect fires on initial mount
   with manualCollapse=false, so the spy captured 'false' before the click's
   effect ran. Fix: move spy setup after render(), add await setTimeout(0) to
   flush Svelte effects before asserting.

3. DashboardMentions — component now renders a persistent
   "Benachrichtigungsverlauf ansehen" link, making getByRole('link') strict-
   mode violations. Fix: scope link queries to the actor name, and check
   absence of the actor link (not all links) in the no-documentId test.

4. Conversations page — empty-state copy changed from "Wählen Sie zwei
   Personen aus" to "Korrespondenz durchsuchen". Update the test.

5. Login page — AuthHeader adds a second aria-label="Familienarchiv" link.
   Use .first() to avoid strict-mode violation.

6. Persons page — alias is rendered with German quotation marks „…" not
   straight quotes "…". Update the test string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 17:28:35 +02:00
Marcel
9d6c7b8605 test(DateInput): add Vitest specs for DateInput component and date utils
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 4m3s
CI / Backend Unit Tests (pull_request) Failing after 2m24s
CI / E2E Tests (pull_request) Failing after 1h46m24s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 18:20:07 +02:00
Marcel
169e6dc578 chore: merge main into feat/persons-redesign-concept-a
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
Resolved conflicts in messages/de.json, en.json, es.json by keeping
both the persons-redesign keys (feature branch) and the notification
keys (main) in all three locale files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 21:30:54 +02:00
Marcel
3abdf9bb68 feat(persons): add formatLifeDateRange + formatDocumentStatus utility functions
Unit tests for both; i18n keys for doc status and person stats bar;
PERSON_NOT_FOUND added to frontend ErrorCode type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 19:50:30 +02:00
Marcel
c8f7225506 refactor(notifications): extract NotificationItem type and relativeTime to shared utility
Extracted from NotificationBell.svelte into $lib/utils/notifications.ts so the
history page can reuse them. relativeTime() now accepts an optional `now` param
for deterministic unit testing. Added parseNotificationEvent() for SSE payload
shape validation (NullX Finding 3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 19:12:14 +02:00
Marcel
9900d0b54b test: add AnnotationSidePanel spec and fix env mock in layout spec
Some checks failed
CI / Unit & Component Tests (push) Successful in 3m47s
CI / Backend Unit Tests (push) Successful in 2m41s
CI / E2E Tests (push) Failing after 2h25m30s
CI / Unit & Component Tests (pull_request) Successful in 2m48s
CI / Backend Unit Tests (pull_request) Successful in 2m29s
CI / E2E Tests (pull_request) Failing after 2h29m1s
- AnnotationSidePanel: cover visibility (null vs set annotationId),
  close button callback, and targetCommentId forwarding
- layout.svelte.spec: mock $env/static/public to satisfy
  PUBLIC_NOTIFICATION_POLL_MS import from NotificationBell
- mention.spec: update assertion to match span-based mention rendering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 11:46:27 +01:00
Marcel
dc6ea080c4 fix(#71-#73): address all review findings from Markus and Sara
BLOCKERs:
- Remove direct AppUserRepository/CommentRepository access from CommentService and
  NotificationService — replaced with UserService.findAllById() and UserService
  (fixes layering contract from CLAUDE.md)
- Switch Optional<JavaMailSender> constructor injection — removes @Autowired(required=false)
  field and ReflectionTestUtils hack in tests
- Add @RequirePermission(READ_ALL) to UserSearchController — prevents user enumeration
  without read access

Data bug:
- Promote actorName from @Transient to persisted VARCHAR column (V18 migration)
- Set actorName in notifyReply and notifyMentions from comment.getAuthorName()

Architecture:
- Add @RequirePermission(READ_ALL) to NotificationController
- Introduce NotificationDTO — controller returns DTO instead of Notification entity,
  eliminating lazy-load N+1 and AppUser field leakage
- Change mentions FetchType to EAGER — fixes LazyInitializationException outside transaction
- Add @Transactional(propagation=REQUIRES_NEW) to notifyReply/notifyMentions so a
  notification failure cannot roll back the parent comment
- N+1 fix: replace per-ID findById loops with single findAllById bulk fetch
- Move collectParticipantIds to CommentService; notifyReply accepts Set<UUID> directly

Security:
- Escape displayName before injecting into renderBody HTML span
- Replace <a href="#"> with <span class="mention"> — no profile page to link to, and
  the anchor's scroll-to-top behaviour is harmful

Tests added/fixed:
- markRead_throwsNotFound, markAllRead_delegatesToRepository, countUnread_delegatesToRepository
- markOneRead_returns401, @RequirePermission 403 coverage for both controllers
- postComment/replyToComment_triggersNotifyMentions_whenMentionedUserIdsProvided
- search_returnsAtMostTenResults now asserts $.length() <= 10
- XSS regression test for escaped displayName in mention.spec.ts

Frontend minors:
- relativeTime() uses Intl.RelativeTimeFormat (locale-aware, not German-hardcoded)
- aria-label uses m.notification_unread() Paraglide key (de/en/es added)
- <div role="button"> replaced with <button> (native Enter+Space handling)
- onDestroy clears debounceTimer in MentionEditor
- setTimeout(100) replaced with await tick() + requestAnimationFrame in CommentThread
- Notification prefs form uses checkbox name attributes + formData.has() pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 00:31:38 +01:00
Marcel
55cf1fb0a4 feat(#72): add @mention support in comment editor
- mention.ts: detectMention (cursor-aware), extractContent (parse @Name → UUID), renderBody (XSS-safe: escape-first then inject anchor tags, replaceAll for all occurrences)
- 19 unit tests in mention.spec.ts (all green)
- MentionEditor.svelte: textarea with @-trigger popup, debounced /api/users/search, keyboard navigation (↑↓ Enter Esc), Ctrl+Enter submit, @ button for accessibility
- CommentThread.svelte: replace plain textareas with MentionEditor, send mentionedUserIds on post/reply/edit, render comment bodies with {@html renderBody(...)}
- types.ts: add MentionDTO, add optional mentionDTOs to Comment and CommentReply

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 20:32:54 +01:00
Marcel
f0940524e7 feat(filename): support compound last names like de Gruyter
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 2m17s
CI / Backend Unit Tests (pull_request) Successful in 2m13s
CI / E2E Tests (pull_request) Failing after 25m0s
Replace the four fixed regexes with a split-based algorithm:
- first segment = date → last segment = firstName, rest = lastName parts
- last segment = date → second-to-last = firstName, rest = lastName parts

18881025_de_Gruyter_Walter.pdf now correctly yields "Walter de Gruyter".
Simple two-segment names behave identically to before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 15:33:21 +01:00
Marcel
8555193a79 feat(filename): add parseFilename utility with full-pattern-only matching
Supports four patterns: date_lastname_firstname and lastname_firstname_date,
both with ISO (YYYY-MM-DD) and compact (YYYYMMDD) date formats.
Returns dateIso, personName and a formatted suggestedTitle.
Partial matches are rejected — unrecognised filenames return {}.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 15:17:16 +01:00
Marcel
d40d4b21e1 refactor(utils): consolidate date utilities into \$lib/utils/date.ts
Move isoToGerman and germanToIso from utils.ts into utils/date.ts alongside
formatDate, and add handleGermanDateInput for the shared date field handler.
Make utils.ts a re-export shim so existing imports continue to work.

Closes part of #75

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 12:07:46 +01:00
Marcel
db2fc33e99 fix(frontend): enforce lint locally and in CI, fix all pre-existing violations
Some checks failed
CI / Unit & Component Tests (push) Successful in 1m59s
CI / E2E Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
## Pre-commit hook
- Add .husky/pre-commit at repo root: runs `cd frontend && npm run lint`
- Update prepare script in package.json to auto-configure git hooks path
  on npm install (git -C .. config core.hooksPath .husky)
- Add lint step to CI unit-tests job so it catches issues before tests run
- Add generated dirs to .prettierignore (paraglide_bak*, test-results, .auth)
- Add src/lib/paraglide_bak* to .gitignore so ESLint can ignore them

## ESLint fixes (all pre-existing)
- Disable svelte/no-navigation-without-resolve: false positive in SvelteKit
  (rule targets Svelte 5 standalone routing, not SvelteKit <a href>)
- Fix svelte/require-each-key: add (item.id)/(item) keys to all {#each} blocks
  across 10 files — improves Svelte reconciliation performance
- Fix svelte/prefer-writable-derived in PersonTypeahead: $state+$effect → $derived
- Fix svelte/prefer-svelte-reactivity: URLSearchParams → SvelteURLSearchParams,
  Map → SvelteMap (enables Svelte reactive tracking)
- Fix @typescript-eslint/no-unused-vars: remove dead imports/variables

## Prettier
- Run npm run format to bring all source files in line with .prettierrc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 15:55:42 +01:00
Marcel
4771832492 refactor(frontend): extract toActionResult helper and formatDate utility
- Admin page: replace 7 identical error-handling blocks with a single
  toActionResult() helper — DRY without over-abstraction
- New date.ts util: formatDate(isoDate) centralises the T12:00:00
  timezone guard and Intl.DateTimeFormat locale config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 12:10:04 +01:00
Marcel
fb08eb30a4 feat(persons): add sort toggle to person document list (issue #24)
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 1m55s
CI / Backend Unit Tests (pull_request) Successful in 2m9s
CI / E2E Tests (pull_request) Successful in 18m8s
Extracted sortDocumentsByDate utility with full Vitest coverage (6 tests),
wired it into the person detail page with a DESC/ASC toggle button, and
added an E2E smoke test for the toggle interaction.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 21:24:46 +01:00