Commit Graph

391 Commits

Author SHA1 Message Date
Marcel
3b1317af98 fix(admin-tags): name clears after save and wrong confirmation text
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m30s
CI / Backend Unit Tests (pull_request) Failing after 2m45s
CI / Unit & Component Tests (push) Failing after 2m35s
CI / Backend Unit Tests (push) Failing after 2m44s
SvelteKit's use:enhance resets the form after a successful action.
The name input used value={data.tag.name} without bind:, so Svelte 5's
fine-grained reactivity did not re-apply the unchanged value after the
reset — leaving the field empty. Passing reset: false to update() fixes
this.

Also corrected the confirmation message from "renamed" to "saved" in
all three locales, since the action updates name, parent, and color.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 10:22:54 +02:00
Marcel
4442b25a7a fix(#248): add confirmation dialog before tag delete
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m29s
CI / Backend Unit Tests (push) Failing after 2m43s
CI / Unit & Component Tests (pull_request) Failing after 2m36s
CI / Backend Unit Tests (pull_request) Failing after 2m34s
TagDeleteGuard now calls confirm() (admin_tag_delete_confirm) before
submitting — same pattern as document delete. Button changed to type=button
with an async handler; page.svelte.spec.ts updated to pass ConfirmService
context so TagDeleteGuard can initialise inside the page render.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 09:39:33 +02:00
Marcel
47d57b96c8 fix(#248): show merge success banner via PRG pattern (?merged=1 redirect)
After a successful merge, redirect 303 to /admin/tags/{targetId}?merged=1.
Load function detects the param and returns mergeSuccess:true; +page.svelte
renders the banner and cleans the URL with replaceState so refresh doesn't
re-show it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 09:15:29 +02:00
Marcel
902172e4e2 fix(#248): fix 3 merge zone bugs — stale state, wrong placeholder, missing success feedback
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m41s
CI / Backend Unit Tests (pull_request) Failing after 2m34s
CI / Unit & Component Tests (push) Failing after 2m21s
CI / Backend Unit Tests (push) Failing after 2m35s
- TagMergeZone: add $effect to reset targetId when tag prop changes (fixes stale form after navigation)
- TagMergeZone: pass merge-specific placeholder to TagParentPicker
- TagMergeZone: show success banner on form.mergeSuccess and goto() target tag
- +page.server.ts: merge action returns { mergeSuccess, mergeTargetId } instead of redirect

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 08:12:35 +02:00
Marcel
aff7afa7cb fix(#248): resolve parent UUID to name in TagParentPicker dropdown subtitle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 07:42:13 +02:00
Marcel
be7009f9ed fix(#248): replace document.querySelectorAll with page.getByRole in TagDeleteGuard spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 07:39:06 +02:00
Marcel
ba8758c085 fix(#248): mode-aware delete button text in TagDeleteGuard and fix document.querySelector in spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:58:50 +02:00
Marcel
61976e9479 fix(#248): increase tree-node indent from 12px to 16px for better scanability
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:43:44 +02:00
Marcel
7919ba3a57 fix(#248): address PR review concerns — i18n, aria-label, stable keys, test selectors
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m37s
CI / Backend Unit Tests (push) Failing after 2m48s
CI / Unit & Component Tests (pull_request) Failing after 2m35s
CI / Backend Unit Tests (pull_request) Failing after 2m49s
- Add filter_operator_and/or/and_label/or_label i18n keys to de/en/es locale files
- Add aria-label and aria-pressed to AND/OR toggle buttons in SearchFilterBar
- Add data-testid="operator-and/or" for unambiguous test targeting (fixes substring match on German "Schlagwort")
- Use stable keys (tag.id ?? tag.name) for TagInput chip and suggestion lists
- Remove aria-level from role="option" items in TagInput (invalid attribute for that role)
- Add aria-live="polite" role="status" to TagMergeZone step indicator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:24:53 +02:00
Marcel
172c5613ed feat(#248): overhaul tag edit page — TagParentPicker, new components, merge+subtree actions
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m51s
CI / Backend Unit Tests (push) Failing after 2m46s
CI / Unit & Component Tests (pull_request) Failing after 2m39s
CI / Backend Unit Tests (pull_request) Failing after 2m58s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 23:46:06 +02:00
Marcel
f1889ff20c feat(#248): add TagDeleteGuard component and brand-warning CSS token
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 23:34:29 +02:00
Marcel
4d670de156 feat(#248): add TagMergeZone component with 2-step merge flow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 23:30:28 +02:00
Marcel
b6b1b142dc feat(#248): add TagAncestry and TagChildrenPreview components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 23:26:02 +02:00
Marcel
97fbf1e4ca feat(#248): replace flat TagsListPanel with collapsible ARIA tree (TagTreeNode)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 22:57:45 +02:00
Marcel
9b5af67780 feat(#248): switch layout load to GET /api/tags/tree, expose tree + flat tags
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 22:40:23 +02:00
Marcel
171f06da22 fix(#221): reset parent/color/delete state when navigating between tag edit pages
SvelteKit reuses the same +page.svelte instance on client-side navigation,
so $state() initialisations only run on mount. Add an $effect keyed on
data.tag.id to reset parentId, selectedColor and deleteConfirmName whenever
the user switches to a different tag in the admin sidebar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 19:14:28 +02:00
Marcel
89949977c7 fix(#221): suppress tagQ when tags are already selected
Sending tagQ alongside selected tags caused an unintended AND: documents
had to match both the selected-tag filter and the partial-name filter,
making the list shrink while the user was still typing a new tag.

tagQ is now only forwarded to the backend when no tags are selected,
which is the only case where the live partial-filter is meaningful.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 18:18:25 +02:00
Marcel
532692e0fb fix(#221): bypass debounce on AND/OR operator toggle to prevent race condition
The tag-change $effect called triggerSearch() immediately (no debounce).
When the user toggled AND/OR within the 500 ms debounce window, the prior
navigation would complete and reset tagOperator back to AND before the
debounced search fired. The toggle now calls onSearchImmediate, which
clears any pending timer and fires triggerSearch() synchronously.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 17:24:30 +02:00
Marcel
7f53651f13 feat(#221): render tag list hierarchically with indentation and color dots
TagsListPanel now accepts optional parentId/color on each Tag. A
$derived.by walk produces an ordered flat list with depth annotations.
Child tags are indented with pl-5; root-level tags with a color get
a colored dot before their name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:46:55 +02:00
Marcel
d900480920 feat(#221): add parent selector and color picker to admin tag edit form
Tag edit form gains a parent <select> listing all other tags (self
excluded) and a 10-swatch color picker that is only shown when no
parent is selected. Submitting passes parentId and color to the PUT
/api/tags/{id} endpoint via TagUpdateDTO.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:39:02 +02:00
Marcel
abba85a451 feat(#221): wire tagOp URL param from server to SearchFilterBar
Reads ?tagOp=OR from URL in +page.server.ts, passes it to the backend
search endpoint, and surfaces it via the filters return. +page.svelte
initialises tagOperator state from filters, writes it back to the URL
in triggerSearch(), and binds it to SearchFilterBar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:25:27 +02:00
Marcel
b54d2b0125 feat(#221): add AND/OR pill toggle to SearchFilterBar tag filter
Toggle appears when ≥2 tags are selected; defaults to AND.
Exposes tagOperator prop ('AND'|'OR') for parent to read via bind.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:20:21 +02:00
Marcel
e03fb38274 feat(#221): add color dot to tag chips in DocumentList
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:14:32 +02:00
Marcel
e8e54cc282 feat(#221): change TagInput binding to Tag[], add color dots and hierarchy grouping
Backend:
- TagRepository: add findDescendantIdsByName() recursive CTE query
- TagService: add expandTagNamesToDescendantIdSets() for document search

Frontend:
- TagInput: accept Tag[] (id, name, color, parentId) instead of string[]
- Chips show color dot via var(--c-tag-{color}) when tag has color
- Suggestions grouped hierarchically: children indented under their parents
- Update DescriptionSection, edit/new pages, SearchFilterBar, +page.svelte

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:11:38 +02:00
Marcel
e4f21bd896 feat(#221): add --c-tag-* CSS custom properties for 10 semantic tag color tokens
Light and dark variants for: sage, sienna, amber, slate, violet, rose,
cobalt, moss, sand, coral — used as decorative dot colors on tag chips.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 15:50:13 +02:00
Marcel
d78685c5a4 fix(#240): accessibility, color consistency, and reviewedPct denominator
- TranscriptionColumn progress bar: add aria-hidden="true" (the block count
  text above already communicates the value to screen readers)
- TranscriptionColumn weekly pulse: text-ink → text-ink-2 (matches
  SegmentationColumn, same semantic element)
- ReadyColumn reviewedPct: align denominator to annotationCount so the
  displayed percentage matches the SQL threshold used to classify "ready"
- page.svelte.spec.ts: add missing segmentationDocs/transcriptionDocs/
  readyDocs/weeklyStats to emptyData fixture

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 12:25:36 +02:00
Marcel
ff1606f63d fix(#240): update test fixtures broken by rebase changes
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m29s
CI / Backend Unit Tests (push) Failing after 2m38s
CI / Unit & Component Tests (pull_request) Failing after 2m31s
CI / Backend Unit Tests (pull_request) Failing after 2m42s
Two backend tests passed a 6-element enrichment row but the rebase
added summary_snippet as column 7 — added null at index 6 to both
fixtures.

Two frontend page.server tests mocked only 4 dashboard API calls but
the page now makes 8 (3 Mission Control queues + weekly-stats added
on this branch) — added the 4 missing mock responses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:50:49 +02:00
Marcel
ca0cf4903c refactor(#240): remove needsExpert feature completely
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m23s
CI / Backend Unit Tests (pull_request) Failing after 2m43s
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (push) Has started running
Drops the needsExpert / needs_expert flag end-to-end: DB migration
(V37, never applied), Document entity field, PATCH endpoint, service
method, DTO field, all three queue queries, ExpertBadge component,
i18n key, generated API types, and test fixture.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:52:14 +02:00
Marcel
f36bebd1a8 feat(#240): Mission Control Strip frontend — 5 components + dashboard wiring
Adds the full-width 3-column collaboration widget below the existing
dashboard grid. Renders without the backend running (Promise.allSettled
isolation keeps failures silent).

Components (src/lib/components/):
- ExpertBadge.svelte — purple pill with icon, no props
- SegmentationColumn.svelte — col 1: links to /enrich/{id}, weekly pulse
- TranscriptionColumn.svelte — col 2: per-doc progress bar when blocks exist
- ReadyColumn.svelte — col 3: mint border when filled, dashed empty state
- MissionControlStrip.svelte — strip wrapper, 1-col mobile / 3-col sm+

i18n: 19 new keys added to de/en/es (mission_control_*)

Page wiring:
- +page.server.ts: 4 new Promise.allSettled calls for segmentation-queue,
  transcription-queue, ready-to-read, weekly-stats; all failures silent
- +page.svelte: MissionControlStrip rendered below the grid in isDashboard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:42:07 +02:00
Marcel
53c5d90340 feat(#240): update generated API types for Mission Control Strip
Manually adds the new types to src/lib/generated/api.ts:
- Document.needsExpert: boolean (required field)
- TranscriptionQueueItemDTO schema
- TranscriptionWeeklyStatsDTO schema
- Paths: /api/transcription/{segmentation-queue, transcription-queue,
         ready-to-read, weekly-stats} and /api/documents/{id}/needs-expert
- Operations: matching typed request/response shapes

Fixes briefwechsel spec fixtures to include scriptType and needsExpert
so the Document type shape is satisfied.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:41:55 +02:00
Marcel
d7b2357834 feat(search): surface summary snippet when summary matched the query
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m33s
CI / Backend Unit Tests (push) Failing after 2m44s
Add a summary_snippet column to findEnrichmentData using ts_headline on
documents.summary, only when the summary's tsvector matches the query.
Expose it via SearchMatchData.summarySnippet / summaryOffsets and render
a "Zusammenfassung" / "Summary" / "Resumen" labelled row in the document
list — identical treatment to the transcription snippet row.

Fixes the case where a document appeared in search results with no
visible match explanation (e.g. searching "frucht" found a document
whose summary mentioned "Früchte").

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 09:10:10 +02:00
Marcel
eb18d4f568 feat(search): restyle highlights to navy underline and add snippet labels
Switch search match highlights from bordered mint chips to a plain navy
underline (decoration-brand-navy). Add visible "Inhalt" / "Content" /
"Contenido" label before the transcription snippet, matching the style
of the Von/An sender-receiver labels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 09:10:10 +02:00
Marcel
9ff8423da6 feat(search): highlight snippet terms and mark sender/receiver/tag matches in document list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 09:10:10 +02:00
Marcel
2c64a6d8a4 style(search): improve mark hover contrast, remove no-op class, italicize snippet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 09:10:10 +02:00
Marcel
bb29cac496 feat(search): pass matchData from server load to DocumentList
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 09:10:10 +02:00
Marcel
60dc73ba04 feat(search): render title highlights and transcription snippets in DocumentList
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 09:10:10 +02:00
Marcel
45490ebaac fix(a11y): increase nav label font size from 9px to 11px in EntityNavSection
text-[9px] is below WCAG practical minimum and unreadable for senior users.
Changed all three occurrences (tablet button count, desktop link label,
flyout link label) to text-[11px].

Fixes @leonievoss: "text-[9px] is below 12px minimum"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 15:16:37 +02:00
Marcel
fe6c247882 refactor(admin): extract EntityNavSection to eliminate nav markup repetition (#197)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 13:54:42 +02:00
Marcel
accfa5373e refactor(unsaved): extract createUnsavedWarning hook and UnsavedWarningBanner
Move the identical isDirty / beforeNavigate / discard pattern out of the
three admin detail pages (groups, tags, users) into a reusable
createUnsavedWarning() hook and a UnsavedWarningBanner presentational
component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 13:31:17 +02:00
Marcel
34e7436fdc refactor(fileloader): extract createFileLoader hook from document/enrich pages
Move blob URL lifecycle management into a reusable createFileLoader()
hook that owns revoke-before-create and revoke-on-destroy. Replace
identical inline logic in documents/[id] and enrich/[id] with the hook.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 13:20:32 +02:00
Marcel
dbf7f0bc16 fix(fileloader): revoke blob URLs before re-assignment and on destroy
Calling loadFile a second time previously leaked the previous object URL.
Add URL.revokeObjectURL(fileUrl) before creating a new one and in
onDestroy so all URLs are freed. Revoke behavior will be covered by the
useFileLoader hook tests in the next commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 13:13:21 +02:00
Marcel
731cdc75ab refactor(frontend): delete dead conversations/ route (#193)
Remove the old conversations page that was superseded by briefwechsel/.
No navigation link pointed to /conversations; it was unreachable through
the UI. Deletes 5 files, removes 14 orphaned i18n keys from de/en/es
message bundles, and removes E2E tests that navigated to /conversations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 12:43:40 +02:00
Marcel
38d558182a refactor(conversations): migrate ConversationTimeline to groupDocuments
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 1s
CI / Backend Unit Tests (pull_request) Failing after 2s
CI / Unit & Component Tests (push) Failing after 3s
CI / Backend Unit Tests (push) Failing after 2s
Replace hand-rolled enrichedDocuments year-divider logic with the shared
groupDocuments utility. Also fixes a timezone bug in documentYears: adds
'T12:00:00' to date strings so getFullYear() doesn't drift on UTC boundaries.
No behavior change — year dividers render the same way as before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 09:41:40 +02:00
Marcel
25aa05411f fix(server): allowlist dir param in page.server.ts
Mirrors the existing sort allowlist pattern. Any value other than 'asc' or
'desc' silently falls back to 'desc', preventing arbitrary strings from
reaching the search API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 09:39:24 +02:00
Marcel
593a6c8a38 test+fix(docs): correct fallbackLabel when sort prop is omitted
Add failing test for DATE-sort + undated doc showing "Undatiert" fallback
label, then fix DocumentList by null-coalescing sort before comparison
((sort ?? 'DATE') === 'DATE'). Test uses one dated + one undated doc to
produce two groups and trigger GroupDivider rendering.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 09:37:19 +02:00
Marcel
67c03dab8c feat(search): wire sort to DocumentList; validate sort param allowlist
Some checks failed
CI / Unit & Component Tests (push) Failing after 3s
CI / Backend Unit Tests (push) Failing after 0s
CI / Unit & Component Tests (pull_request) Failing after 2s
CI / Backend Unit Tests (pull_request) Failing after 1s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 08:00:09 +02:00
Marcel
e302d3d689 feat(search): add group headers to DocumentList by sort field
Documents sorted by DATE show year dividers, SENDER/RECEIVER sort
shows person name dividers. Dividers only appear when there are 2+
distinct groups. Multi-receiver docs appear in each receiver group.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 07:59:02 +02:00
Marcel
ce2bbf4230 refactor(conversations): use GroupDivider in ConversationTimeline
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 23:35:09 +02:00
Marcel
1fd5c31fd1 fix(training): pass trainingInfo directly to SegmentationTrainingCard
The parent was manually remapping availableSegBlocks → availableBlocks
before passing props, which broke after the card was updated to read
availableSegBlocks directly. Pass the full trainingInfo object instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 17:55:16 +02:00
Marcel
ee58b63517 feat(ocr): add guided OCR mode using existing annotation regions
When a document has manually drawn annotation boxes, the user can now
enable "Nur annotierte Bereiche" in the OCR trigger panel. The engine
skips layout detection entirely and runs recognition only within the
pre-drawn bounding boxes, preserving manual transcription blocks.

- Python: adds OcrRegion model, extend OcrRequest/OcrBlock; guided
  branch in /ocr/stream groups by page and crops each region
- Engines: add extract_region_text() to both Kraken and Surya
- Java: adds OcrBlockResult.annotationId, OcrClient.OcrRegion,
  TriggerOcrDTO.useExistingAnnotations; OcrAsyncRunner dispatches to
  upsertGuidedBlock when annotationId is present; OcrService threads
  the flag through to runSingleDocument
- TranscriptionService: adds upsertGuidedBlock (creates, updates OCR,
  or preserves MANUAL blocks)
- Frontend: guided OCR toggle in OcrTrigger shown when blocks exist;
  skips destructive-replace confirmation in guided mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 15:57:54 +02:00