From 4e28f2f31b4dee53c7f487af8c61696115e37994 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 10 Jun 2026 00:01:42 +0200 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20regen=20API=20types=20?= =?UTF-8?q?=E2=80=94=20migrate=20consumers=20off=20dropped=20Geschichte=20?= =?UTF-8?q?schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With create/update returning GeschichteView, no endpoint serves the raw Geschichte entity and springdoc drops its schema. Dashboard modules and the home loader now use GeschichteSummary; GeschichteEditor takes GeschichteView and maps persons into the displayName shape PersonMultiSelect renders — fixing blank person chips on story edit. PersonMultiSelect/Sidebar narrow to Pick, mirroring the DocumentOption precedent. Co-Authored-By: Claude Fable 5 --- frontend/src/lib/generated/api.ts | 155 +++++++++--------- .../lib/geschichte/GeschichteEditor.svelte | 14 +- .../lib/geschichte/GeschichteSidebar.svelte | 3 +- .../src/lib/person/PersonMultiSelect.svelte | 5 +- .../dashboard/ReaderDraftsModule.svelte | 4 +- .../ReaderDraftsModule.svelte.spec.ts | 9 +- .../dashboard/ReaderRecentStories.svelte | 4 +- .../ReaderRecentStories.svelte.spec.ts | 9 +- frontend/src/routes/+page.server.ts | 10 +- 9 files changed, 108 insertions(+), 105 deletions(-) diff --git a/frontend/src/lib/generated/api.ts b/frontend/src/lib/generated/api.ts index ceb29071..726682be 100644 --- a/frontend/src/lib/generated/api.ts +++ b/frontend/src/lib/generated/api.ts @@ -728,6 +728,22 @@ export interface paths { patch?: never; trace?: never; }; + "/api/admin/backfill-titles": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["backfillTitles"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/admin/backfill-file-hashes": { parameters: { query?: never; @@ -1464,22 +1480,6 @@ export interface paths { patch?: never; trace?: never; }; - "/api/documents/conversation": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["getConversation"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/api/dashboard/resume": { parameters: { query?: never; @@ -1836,6 +1836,7 @@ export interface components { sender?: components["schemas"]["Person"]; tags?: components["schemas"]["Tag"][]; trainingLabels?: ("KURRENT_RECOGNITION" | "KURRENT_SEGMENTATION")[]; + hasTranscription: boolean; thumbnailUrl?: string; }; PersonMention: { @@ -2023,25 +2024,44 @@ export interface components { body?: string; /** @enum {string} */ status?: "DRAFT" | "PUBLISHED"; + /** @enum {string} */ + type?: "STORY" | "JOURNEY"; personIds?: string[]; - documentIds?: string[]; }; - Geschichte: { + AuthorView: { + /** Format: uuid */ + id: string; + displayName: string; + }; + GeschichteView: { /** Format: uuid */ id: string; title: string; body?: string; /** @enum {string} */ status: "DRAFT" | "PUBLISHED"; - author?: components["schemas"]["AppUser"]; - persons?: components["schemas"]["Person"][]; - documents?: components["schemas"]["Document"][]; + /** @enum {string} */ + type: "STORY" | "JOURNEY"; + author?: components["schemas"]["AuthorView"]; + persons: components["schemas"]["PersonView"][]; + items: components["schemas"]["JourneyItemView"][]; + /** Format: date-time */ + publishedAt?: string; /** Format: date-time */ createdAt: string; /** Format: date-time */ updatedAt: string; - /** Format: date-time */ - publishedAt?: string; + }; + PersonView: { + /** Format: uuid */ + id: string; + firstName?: string; + lastName?: string; + }; + JourneyItemCreateDTO: { + /** Format: uuid */ + documentId?: string; + note?: string; }; CreateTranscriptionBlockDTO: { /** Format: int32 */ @@ -2311,6 +2331,11 @@ export interface components { color?: string; /** Format: int32 */ documentCount: number; + /** + * Format: int32 + * @description Distinct documents tagged with this tag or any descendant tag (subtree rollup) + */ + subtreeDocumentCount: number; children?: components["schemas"]["TagTreeNodeDTO"][]; /** * Format: uuid @@ -2497,40 +2522,12 @@ export interface components { type: "STORY" | "JOURNEY"; /** @enum {string} */ status: "DRAFT" | "PUBLISHED"; + /** Format: date-time */ + updatedAt: string; author?: components["schemas"]["AuthorSummary"]; /** Format: date-time */ publishedAt?: string; }; - AuthorView: { - /** Format: uuid */ - id: string; - displayName: string; - }; - GeschichteView: { - /** Format: uuid */ - id: string; - title: string; - body?: string; - /** @enum {string} */ - status: "DRAFT" | "PUBLISHED"; - /** @enum {string} */ - type: "STORY" | "JOURNEY"; - author?: components["schemas"]["AuthorView"]; - persons: components["schemas"]["PersonView"][]; - items: components["schemas"]["JourneyItemView"][]; - /** Format: date-time */ - publishedAt?: string; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - updatedAt: string; - }; - PersonView: { - /** Format: uuid */ - id: string; - firstName?: string; - lastName?: string; - }; DocumentVersionSummary: { /** Format: uuid */ id: string; @@ -3733,7 +3730,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Geschichte"][]; + "*/*": components["schemas"]["GeschichteSummary"][]; }; }; }; @@ -3757,7 +3754,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Geschichte"]; + "*/*": components["schemas"]["GeschichteView"]; }; }; }; @@ -4286,6 +4283,26 @@ export interface operations { }; }; }; + backfillTitles: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["BackfillResult"]; + }; + }; + }; + }; backfillFileHashes: { parameters: { query?: never; @@ -4485,7 +4502,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["Geschichte"]; + "*/*": components["schemas"]["GeschichteView"]; }; }; }; @@ -5476,32 +5493,6 @@ export interface operations { }; }; }; - getConversation: { - parameters: { - query: { - senderId: string; - receiverId?: string; - from?: string; - to?: string; - dir?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "*/*": components["schemas"]["Document"][]; - }; - }; - }; - }; getResume: { parameters: { query?: never; diff --git a/frontend/src/lib/geschichte/GeschichteEditor.svelte b/frontend/src/lib/geschichte/GeschichteEditor.svelte index c325deba..962f91cf 100644 --- a/frontend/src/lib/geschichte/GeschichteEditor.svelte +++ b/frontend/src/lib/geschichte/GeschichteEditor.svelte @@ -7,11 +7,12 @@ import { m } from '$lib/paraglide/messages.js'; import type { components } from '$lib/generated/api'; import GeschichteSidebar from '$lib/geschichte/GeschichteSidebar.svelte'; -type Geschichte = components['schemas']['Geschichte']; +type GeschichteView = components['schemas']['GeschichteView']; type Person = components['schemas']['Person']; +type PersonOption = Pick; interface Props { - geschichte?: Geschichte | null; + geschichte?: GeschichteView | null; initialPersons?: Person[]; onSubmit: (payload: { title: string; @@ -31,8 +32,13 @@ let { geschichte = null, initialPersons = [], onSubmit, submitting = false }: Pr let title = $state(geschichte?.title ?? ''); let body = $state(geschichte?.body ?? ''); let status: 'DRAFT' | 'PUBLISHED' = $state(geschichte?.status ?? 'DRAFT'); -let selectedPersons: Person[] = $state( - geschichte?.persons ? Array.from(geschichte.persons) : initialPersons +let selectedPersons: PersonOption[] = $state( + geschichte?.persons + ? Array.from(geschichte.persons).map((p) => ({ + id: p.id, + displayName: [p.firstName, p.lastName].filter(Boolean).join(' ') + })) + : initialPersons ); let dirty = $state(false); diff --git a/frontend/src/lib/geschichte/GeschichteSidebar.svelte b/frontend/src/lib/geschichte/GeschichteSidebar.svelte index c80e64c5..4a149b17 100644 --- a/frontend/src/lib/geschichte/GeschichteSidebar.svelte +++ b/frontend/src/lib/geschichte/GeschichteSidebar.svelte @@ -4,10 +4,11 @@ import type { components } from '$lib/generated/api'; import PersonMultiSelect from '$lib/person/PersonMultiSelect.svelte'; type Person = components['schemas']['Person']; +type PersonOption = Pick; interface Props { status: 'DRAFT' | 'PUBLISHED'; - selectedPersons: Person[]; + selectedPersons: PersonOption[]; } let { status, selectedPersons = $bindable() }: Props = $props(); diff --git a/frontend/src/lib/person/PersonMultiSelect.svelte b/frontend/src/lib/person/PersonMultiSelect.svelte index 41b392a0..2908e597 100644 --- a/frontend/src/lib/person/PersonMultiSelect.svelte +++ b/frontend/src/lib/person/PersonMultiSelect.svelte @@ -3,9 +3,12 @@ import type { components } from '$lib/generated/api'; import { m } from '$lib/paraglide/messages.js'; import { clickOutside } from '$lib/shared/actions/clickOutside'; type Person = components['schemas']['Person']; +// Narrow contract: only what the chips render and dedup needs. Full Person +// objects from /api/persons remain assignable; view projections fit too. +type PersonOption = Pick; interface Props { - selectedPersons?: Person[]; + selectedPersons?: PersonOption[]; } let { selectedPersons = $bindable([]) }: Props = $props(); diff --git a/frontend/src/lib/shared/dashboard/ReaderDraftsModule.svelte b/frontend/src/lib/shared/dashboard/ReaderDraftsModule.svelte index 703ad0b3..8b9275b1 100644 --- a/frontend/src/lib/shared/dashboard/ReaderDraftsModule.svelte +++ b/frontend/src/lib/shared/dashboard/ReaderDraftsModule.svelte @@ -3,10 +3,10 @@ import * as m from '$lib/paraglide/messages.js'; import { relativeTimeDe } from '$lib/shared/relativeTime'; import type { components } from '$lib/generated/api'; -type Geschichte = components['schemas']['Geschichte']; +type GeschichteSummary = components['schemas']['GeschichteSummary']; interface Props { - drafts: Geschichte[]; + drafts: GeschichteSummary[]; } const { drafts }: Props = $props(); diff --git a/frontend/src/lib/shared/dashboard/ReaderDraftsModule.svelte.spec.ts b/frontend/src/lib/shared/dashboard/ReaderDraftsModule.svelte.spec.ts index 0da32564..983764fd 100644 --- a/frontend/src/lib/shared/dashboard/ReaderDraftsModule.svelte.spec.ts +++ b/frontend/src/lib/shared/dashboard/ReaderDraftsModule.svelte.spec.ts @@ -5,24 +5,25 @@ import { page } from 'vitest/browser'; import ReaderDraftsModule from './ReaderDraftsModule.svelte'; import type { components } from '$lib/generated/api'; -type Geschichte = components['schemas']['Geschichte']; +type GeschichteSummary = components['schemas']['GeschichteSummary']; afterEach(() => { cleanup(); }); -const draft1: Geschichte = { +const draft1: GeschichteSummary = { id: 'g1', title: 'Mein erster Entwurf', status: 'DRAFT', - createdAt: '2025-01-01T00:00:00Z', + type: 'STORY', updatedAt: '2025-01-02T00:00:00Z' }; -const draft2: Geschichte = { +const draft2: GeschichteSummary = { id: 'g2', title: 'Zweiter Entwurf', status: 'DRAFT', + type: 'STORY', createdAt: '2025-02-01T00:00:00Z', updatedAt: '2025-02-01T00:00:00Z' }; diff --git a/frontend/src/lib/shared/dashboard/ReaderRecentStories.svelte b/frontend/src/lib/shared/dashboard/ReaderRecentStories.svelte index 95c446f7..005698a5 100644 --- a/frontend/src/lib/shared/dashboard/ReaderRecentStories.svelte +++ b/frontend/src/lib/shared/dashboard/ReaderRecentStories.svelte @@ -3,10 +3,10 @@ import * as m from '$lib/paraglide/messages.js'; import { relativeTimeDe } from '$lib/shared/relativeTime'; import type { components } from '$lib/generated/api'; -type Geschichte = components['schemas']['Geschichte']; +type GeschichteSummary = components['schemas']['GeschichteSummary']; interface Props { - stories: Geschichte[]; + stories: GeschichteSummary[]; } const { stories }: Props = $props(); diff --git a/frontend/src/lib/shared/dashboard/ReaderRecentStories.svelte.spec.ts b/frontend/src/lib/shared/dashboard/ReaderRecentStories.svelte.spec.ts index c5d83051..a5a669a8 100644 --- a/frontend/src/lib/shared/dashboard/ReaderRecentStories.svelte.spec.ts +++ b/frontend/src/lib/shared/dashboard/ReaderRecentStories.svelte.spec.ts @@ -5,27 +5,28 @@ import { page } from 'vitest/browser'; import ReaderRecentStories from './ReaderRecentStories.svelte'; import type { components } from '$lib/generated/api'; -type Geschichte = components['schemas']['Geschichte']; +type GeschichteSummary = components['schemas']['GeschichteSummary']; afterEach(() => { cleanup(); }); -const story1: Geschichte = { +const story1: GeschichteSummary = { id: 'g1', title: 'Die Familie Müller', body: '

Dies ist eine sehr lange Geschichte über die Familie Müller. Sie lebten in Bayern und hatten viele Kinder. Das war früher so üblich in diesen Gebieten.

', status: 'PUBLISHED', - createdAt: '2025-01-01T00:00:00Z', + type: 'STORY', updatedAt: '2025-01-01T00:00:00Z', publishedAt: '2025-01-01T00:00:00Z' }; -const longBodyStory: Geschichte = { +const longBodyStory: GeschichteSummary = { id: 'g2', title: 'Sehr lange Geschichte', body: '

' + 'A'.repeat(200) + '

', status: 'PUBLISHED', + type: 'STORY', createdAt: '2025-02-01T00:00:00Z', updatedAt: '2025-02-01T00:00:00Z', publishedAt: '2025-02-01T00:00:00Z' diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts index 45e4c5f1..f1aea7d8 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -11,7 +11,7 @@ type ActivityFeedItemDTO = components['schemas']['ActivityFeedItemDTO']; type IncompleteDocumentDTO = components['schemas']['IncompleteDocumentDTO']; type PersonSummaryDTO = components['schemas']['PersonSummaryDTO']; type DocumentListItem = components['schemas']['DocumentListItem']; -type Geschichte = components['schemas']['Geschichte']; +type GeschichteSummary = components['schemas']['GeschichteSummary']; type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO']; function settled(res: PromiseSettledResult | undefined): T | null { @@ -57,9 +57,9 @@ export async function load({ fetch, parent }) { const topPersons = settled<{ items: PersonSummaryDTO[] }>(topPersonsRes)?.items ?? []; const searchData = settled<{ items: DocumentListItem[] }>(recentDocsRes); const recentDocs = searchData?.items ?? []; - const recentStories = settled(recentStoriesRes) ?? []; + const recentStories = settled(recentStoriesRes) ?? []; const tagTree = settled(tagTreeRes) ?? []; - const drafts = settled(draftsRes) ?? []; + const drafts = settled(draftsRes) ?? []; return { isReader: true as const, @@ -179,9 +179,9 @@ export async function load({ fetch, parent }) { readerStats: null, topPersons: [] as PersonSummaryDTO[], recentDocs: [] as DocumentListItem[], - recentStories: [] as Geschichte[], + recentStories: [] as GeschichteSummary[], tagTree: [] as TagTreeNodeDTO[], - drafts: [] as Geschichte[], + drafts: [] as GeschichteSummary[], error: 'Daten konnten nicht geladen werden.' as string | null }; }