- BackButton now accepts a `class` prop (default 'mb-4') so callers can
override spacing; resolves hardcoded margin in flex-row topbar snippets
- documents/[id]/edit and enrich/[id] pass class="" to suppress the margin
- Replace weak className unit test with class-prop behaviour tests
- Add [data-hydrated] comment in E2E spec explaining what emits the attribute
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 7 in-scope back navigation links converted to use history.back().
Admin panel mobile chevron converted inline (icon-only, different
visual pattern). Cancel buttons left as static <a> links.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers: button present, confirm dialog opens, form submitted on confirm,
form not submitted on cancel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract DocumentEditLayout shared component for the PDF+form split-panel
UI, replacing the old scrolling layout on /documents/[id]/edit with the
same fixed-panel structure used by /enrich/[id]. Removes TranscriptionSection
and FileSectionEdit from the edit page; file upload/replace is now handled
by the shared layout. Delete SaveBar and FileSectionEdit as dead code.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- 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>
Matches the FileSectionNew design: upload arrow icon, hidden <input>,
styled label as the click target, shows selected filename on pick.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Long button labels (e.g. German "Speichern & Als überprüft markieren")
require ~515px at text-xs tracking-widest — impossible at 320px inline.
Both save bars (new document + edit document) now use flex-col on mobile
with w-full buttons and flex-row on sm+. Primary actions appear first
(top on mobile, right on desktop). Also fixes hardcoded border-gray-300/
text-gray-600 → border-line/text-ink-2 and bg-brand-navy/text-white →
bg-primary/text-primary-fg in these two components.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In dark mode --c-primary switches from navy (#012851) to mint (#a1dcd8).
Buttons using bg-primary+text-white showed white text on mint at 1.4:1
contrast — invisible. bg-brand-navy buttons were also invisible (navy on
near-black canvas, 1.3:1).
Replaced in 28 components app-wide:
- bg-primary ... text-white → text-primary-fg
- hover:bg-primary hover:text-white → hover:text-primary-fg
- bg-brand-navy ... text-white + hover:bg-brand-navy/90 →
bg-primary ... text-primary-fg + hover:bg-primary/90
Light mode is unchanged: primary-fg = white in light mode.
Dark mode: primary-fg = navy (#012851) on mint bg = readable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
documents.spec.ts: replace getByText with getByRole('heading') to avoid
Svelte's #svelte-announcer matching the same text (strict mode violation).
SaveBar.svelte: move <form id="mark-for-review-form"> out of the component
and into +page.svelte as a sibling of delete-form. The form was previously
nested inside <form id="update-form">, which is invalid HTML. The browser
auto-repaired it, causing a Svelte hydration mismatch that broke the edit
form's use:enhance, preventing version snapshots from being recorded —
leaving history tests with 0 versions instead of the expected 2.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Home page shows "Needs metadata" card when incomplete documents exist.
/enrich list shows all incomplete documents; /enrich/[id] provides a
split PDF-preview + compact form view with Skip / Save / Save & reviewed
actions that auto-advance through the queue.
New document page gets Save vs Save & reviewed split. Edit page gets
"Mark for review" secondary button to push a document back into the queue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add V14 migration: ON DELETE CASCADE for document_tags and document_receivers
so deleting a document removes its join-table rows automatically
- Rename default form action to 'update' in the edit page — SvelteKit forbids
mixing a default action with named actions (was causing 500 on delete)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the subtle link-style delete trigger and broken degruyter icon
with a proper red outlined button and an inline SVG trash bin icon.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DELETE /api/documents/{id} endpoint (204 No Content, WRITE_ALL required)
- DocumentService.deleteDocument() — throws 404 if not found, cascades
via DB foreign keys (versions, annotations, comments all ON DELETE CASCADE)
- Delete form action in edit page server: redirects to / on success
- Two-step confirmation in the save bar: first click reveals inline
"Wirklich löschen?" + confirm/cancel, avoiding native browser dialogs
- i18n key doc_delete_confirm added to de/en/es
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace text-gray-*, bg-gray-*, border-gray-*, divide-gray-*, placeholder-gray-*,
focus:border-blue-*, focus:ring-blue-*, hover:bg-blue-*, and ring-brand-mint with
their semantic-token equivalents (text-ink, bg-muted, border-line, etc.) across
all pages and shared components so dark mode renders correctly everywhere.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces bg-white, text-brand-navy, border-brand-sand, text-gray-*, bg-[#2A2A2A],
bg-brand-purple/15, hover:bg-brand-sand, etc. across all 35 .svelte files with
semantic token utilities (bg-surface, text-ink, border-line, bg-pdf-bg, bg-nav-active,
bg-muted, text-accent, bg-primary, ...).
Also adds CSS filter: invert(1) in layout.css for De Gruyter <img> icons in dark mode,
excluding icons that carry .invert already (to prevent double-inversion).
Closes#64
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No logic changes — whitespace and indentation only. These were flagged
by the pre-commit hook when running lint after layout.css was modified.
Refs #64
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## 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>
Block direct URL navigation to /persons/new, /documents/new,
/documents/:id/edit for users without WRITE_ALL permission.
E2E tests verify admin user retains access to all write routes.
Closes#17
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract all hardcoded German strings from every .svelte file and component
into Paraglide message keys. Add complete translations for all keys in
messages/en.json (English) and messages/es.json (Spanish/Mexico).
Changes:
- messages/de.json: 100+ keys covering navigation, buttons, form labels,
placeholders, section headings, empty states, and error messages
- messages/en.json, messages/es.json: complete translations for all keys
- project.inlang/settings.json: change baseLocale from "en" to "de"
- +layout.svelte: add DE/EN/ES language selector in header using setLocale();
active language is bold, choice persists via Paraglide cookie strategy
- All 10 route pages + 3 shared components: replace hardcoded German with m.key()
- e2e/lang.spec.ts: E2E tests for language selector visibility, switching,
persistence across navigation, and active state highlighting
Closes#2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace `export let` with `$props()` and `$bindable()` across all components
- Replace `$:` reactive statements with `$derived()` and `$effect()`
- Replace `createEventDispatcher` with callback props (e.g. `onchange`)
- Replace `on:event` directives with inline event handlers (`onclick`, `oninput`, etc.)
- Replace `<slot />` with `{@render children()}` in layout
- Use `untrack()` for SSR-safe $state initialization from reactive props
- Replace `blur` + `setTimeout` anti-pattern in TagInput with `clickOutside` action
- Fix `page` store usage in layout to use `$app/state` directly
- 0 errors, 0 warnings after svelte-check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause 1 — OpenAPI types: add @Schema(requiredMode=REQUIRED) to
non-nullable fields on Person, Tag, Document, AppUser, UserGroup;
regenerate api.ts so required fields are no longer optional.
Root cause 2 — Stale types: api.ts regenerated, picking up the Tag
endpoint fix from commit 62189d8 (List<Tag> instead of List<String>).
Root cause 3 — openapi-fetch error pattern: replace `if (apiError)`
(broken when error type is never/undefined) with `if (!result.response.ok)`
across all +page.server.ts files. Cast error via `unknown` to satisfy TS.
Root cause 4 — FormData casts: add `as string` / `as string[]` to
FormData.get() / FormData.getAll() calls in admin/+page.server.ts.
Standalone fixes:
- +page.server.ts: return error field so home page template compiles
- documents/[id]/+page.svelte: type loadFile param, remove invalid iframe `type`
- conversations: type documents as Document[] instead of unknown[]
- persons/[id]: non-null assert person data after ok-check
a11y: aria-label on all icon-only buttons in TagInput and admin page,
replace invalid <label> with <p> for compound controls, remove autofocus.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Show a red border and format hint ("TT.MM.JJJJ") only when the user
has typed something but the date is not yet complete. Focuses-only
or empty fields show no error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace native date picker (mm/dd/yyyy) with a plain text input that
auto-inserts dots as the user types (20122026 → 20.12.2026). A hidden
field transforms the display value back to ISO (yyyy-mm-dd) before
the form is submitted to the backend.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full UX overhaul of the edit page targeting non-technical users:
- Grouped fields into four labelled sections (Wer & Wann, Beschreibung, Transkription, Datei)
- Replaced date text field with native <input type="date">
- Replaced sender <select> with PersonTypeahead component
- Replaced receiver multi-select with new PersonMultiSelect (chip-based)
- Sticky save bar at bottom with cancel/save actions
- Aligned all colours to brand palette (no more blue-*)
- Fixed a11y: replaced invalid <label> on compound components with <p>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All server-side fetch calls now go through createApiClient() from
$lib/api.server.ts, which wraps openapi-fetch with the generated OpenAPI
types. This means backend changes are reflected in the frontend after
running npm run generate:api.
- Add stub src/lib/generated/api.ts (replaced by generate:api output)
- Fix GroupController: missing /api prefix and ResponseStatusException
- Root, conversations, persons, documents pages all use typed client
- Error handling uses apiError.code directly (no parseBackendError needed)
- Edit page load uses typed client; PUT action keeps raw fetch (multipart)
- Login keeps raw fetch (explicit Authorization header, not cookie auth)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend now returns { code: ErrorCode, message: string } for all errors,
making it language-agnostic. Frontend maps codes to localised strings via
Paraglide (en/de/es), so translations live in messages/*.json.
- Add ErrorCode enum and DomainException with static factory methods
- Update GlobalExceptionHandler to return ErrorResponse(code, message)
- Replace ResponseStatusException throughout controllers/services/aspects
- Add frontend errors.ts with parseBackendError() and getErrorMessage()
- getErrorMessage() delegates to Paraglide m.error_*() functions
- Add error_* keys to messages/en.json, de.json, es.json
- Update all page.server.ts files to use the new error utilities
- Fix hardcoded localhost URLs in admin and login pages
- Fix missing baseUrl in deleteTag action
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>