Felix and Elicit both flagged that the isReader formula had no
in-code explanation at the point of definition; future maintainers
adding a new permission level need a fast pointer to the architectural
rationale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Collapses 5x duplicated null-check pattern in the reader fetch branch into
a single typed helper — addresses @Felix review blocker.
Also adds isReader/incompleteDocs/incompleteTotal to page.svelte.spec.ts
baseData so it satisfies the discriminated PageData union introduced by this PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds 5 new components for the permission-gated reader layout:
- ReaderStatsStrip: stat tiles (documents / persons / stories) linking to list pages
- ReaderPersonChips: top-N persons by doc count with avatar + name
- ReaderDraftsModule: blog draft list for BLOG_WRITE users
- ReaderRecentDocs: 5 most-recently-updated docs with Neu/Aktualisiert badge
- ReaderRecentStories: 3 latest published stories with 150-char HTML-stripped excerpt
Each component ships with a vitest-browser spec covering the key assertions.
Avatar color/initials logic is inlined to satisfy $lib/shared → $lib/person
boundary rule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Read-only users (no WRITE_ALL or ANNOTATE_ALL) now receive lean reader
data (stats, top-4 persons, 5 recent docs, 3 recent stories, and drafts
when BLOG_WRITE) instead of the contributor transcription queues.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dashboard loader fetches /api/documents/incomplete?size=5 plus the
existing /incomplete-count and surfaces both via data; +page.svelte
renders EnrichmentBlock with the top 5 docs, the total count, and the
bannerCount state bound to DropZone's onUploadComplete callback
(issue #296).
The block returns null when there is nothing to show, so dashboards
without pending uploads stay uncluttered.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The homepage now always renders the dashboard. Search and browse
moves to the dedicated /documents route (upcoming).
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>
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>
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>
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>
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>
- 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>
- isDashboard was ignoring tagQ so typing in tag filter showed dashboard
- addTag now calls onTextInput('') to clear tagQ when a chip is selected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes /api/notifications from the dashboard widget fetches and replaces
it with /api/stats so the page no longer needs to own notification data.
Returns stats: StatsDTO | null (null on failure) instead of mentions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cast PersonSummaryDTO array to concrete type in +page.server.ts (all
fields are optional in the generated type but always populated at runtime)
- Cast mockLocals/mockLocalsWriter to `any` in persons detail spec to
match the pre-existing test pattern used throughout the codebase
- Add .svelte-kit-backup/ to .gitignore and .prettierignore to prevent
lint failures from Docker-owned leftover .svelte-kit directory
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace recent-by-creation fetch with GET /api/documents/recent-activity
(sorted by updatedAt) in the dashboard. Update DashboardRecentDocuments
component to use doc.updatedAt, update i18n heading to "Zuletzt aktiv" /
"Recent Activity" / "Actividad reciente", and regenerate API types.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add type-only filter to notification repo/service (previously only
worked with type+read=false together)
- Dashboard widget now fetches all recent notifications (mentions +
replies, both read and unread) instead of unread mentions only
- Update component heading and show type label per row
Root cause: Berit's mentions were read=true, so the unread-only filter
returned 0 results. The recent docs widget had no REVIEWED documents
because 'marking ready' sets metadata_complete, not status=REVIEWED.
Recent docs now shows all uploads without a status filter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add isDashboard flag (true when no search filters active)
- In dashboard mode: fetch mentions, incompleteDocs, recentDocs via
Promise.allSettled so widget failures don't crash the page
- In search mode: skip widget fetches for performance
- Replace incomplete-count fetch with list fetch (derive count from
list.length)
- Update enrich page to use IncompleteDocumentDTO (id + title only)
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>
## 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>
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>
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>