refactor(frontend): regen API types — migrate consumers off dropped Geschichte schema

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<Person, 'id' | 'displayName'>, mirroring the DocumentOption precedent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-10 00:01:42 +02:00
parent 282c819e2a
commit 4e28f2f31b
9 changed files with 108 additions and 105 deletions

View File

@@ -728,6 +728,22 @@ export interface paths {
patch?: never; patch?: never;
trace?: 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": { "/api/admin/backfill-file-hashes": {
parameters: { parameters: {
query?: never; query?: never;
@@ -1464,22 +1480,6 @@ export interface paths {
patch?: never; patch?: never;
trace?: 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": { "/api/dashboard/resume": {
parameters: { parameters: {
query?: never; query?: never;
@@ -1836,6 +1836,7 @@ export interface components {
sender?: components["schemas"]["Person"]; sender?: components["schemas"]["Person"];
tags?: components["schemas"]["Tag"][]; tags?: components["schemas"]["Tag"][];
trainingLabels?: ("KURRENT_RECOGNITION" | "KURRENT_SEGMENTATION")[]; trainingLabels?: ("KURRENT_RECOGNITION" | "KURRENT_SEGMENTATION")[];
hasTranscription: boolean;
thumbnailUrl?: string; thumbnailUrl?: string;
}; };
PersonMention: { PersonMention: {
@@ -2023,25 +2024,44 @@ export interface components {
body?: string; body?: string;
/** @enum {string} */ /** @enum {string} */
status?: "DRAFT" | "PUBLISHED"; status?: "DRAFT" | "PUBLISHED";
/** @enum {string} */
type?: "STORY" | "JOURNEY";
personIds?: string[]; personIds?: string[];
documentIds?: string[];
}; };
Geschichte: { AuthorView: {
/** Format: uuid */
id: string;
displayName: string;
};
GeschichteView: {
/** Format: uuid */ /** Format: uuid */
id: string; id: string;
title: string; title: string;
body?: string; body?: string;
/** @enum {string} */ /** @enum {string} */
status: "DRAFT" | "PUBLISHED"; status: "DRAFT" | "PUBLISHED";
author?: components["schemas"]["AppUser"]; /** @enum {string} */
persons?: components["schemas"]["Person"][]; type: "STORY" | "JOURNEY";
documents?: components["schemas"]["Document"][]; author?: components["schemas"]["AuthorView"];
persons: components["schemas"]["PersonView"][];
items: components["schemas"]["JourneyItemView"][];
/** Format: date-time */
publishedAt?: string;
/** Format: date-time */ /** Format: date-time */
createdAt: string; createdAt: string;
/** Format: date-time */ /** Format: date-time */
updatedAt: string; updatedAt: string;
/** Format: date-time */ };
publishedAt?: string; PersonView: {
/** Format: uuid */
id: string;
firstName?: string;
lastName?: string;
};
JourneyItemCreateDTO: {
/** Format: uuid */
documentId?: string;
note?: string;
}; };
CreateTranscriptionBlockDTO: { CreateTranscriptionBlockDTO: {
/** Format: int32 */ /** Format: int32 */
@@ -2311,6 +2331,11 @@ export interface components {
color?: string; color?: string;
/** Format: int32 */ /** Format: int32 */
documentCount: number; documentCount: number;
/**
* Format: int32
* @description Distinct documents tagged with this tag or any descendant tag (subtree rollup)
*/
subtreeDocumentCount: number;
children?: components["schemas"]["TagTreeNodeDTO"][]; children?: components["schemas"]["TagTreeNodeDTO"][];
/** /**
* Format: uuid * Format: uuid
@@ -2497,40 +2522,12 @@ export interface components {
type: "STORY" | "JOURNEY"; type: "STORY" | "JOURNEY";
/** @enum {string} */ /** @enum {string} */
status: "DRAFT" | "PUBLISHED"; status: "DRAFT" | "PUBLISHED";
/** Format: date-time */
updatedAt: string;
author?: components["schemas"]["AuthorSummary"]; author?: components["schemas"]["AuthorSummary"];
/** Format: date-time */ /** Format: date-time */
publishedAt?: string; 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: { DocumentVersionSummary: {
/** Format: uuid */ /** Format: uuid */
id: string; id: string;
@@ -3733,7 +3730,7 @@ export interface operations {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"*/*": components["schemas"]["Geschichte"][]; "*/*": components["schemas"]["GeschichteSummary"][];
}; };
}; };
}; };
@@ -3757,7 +3754,7 @@ export interface operations {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { 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: { backfillFileHashes: {
parameters: { parameters: {
query?: never; query?: never;
@@ -4485,7 +4502,7 @@ export interface operations {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { 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: { getResume: {
parameters: { parameters: {
query?: never; query?: never;

View File

@@ -7,11 +7,12 @@ import { m } from '$lib/paraglide/messages.js';
import type { components } from '$lib/generated/api'; import type { components } from '$lib/generated/api';
import GeschichteSidebar from '$lib/geschichte/GeschichteSidebar.svelte'; import GeschichteSidebar from '$lib/geschichte/GeschichteSidebar.svelte';
type Geschichte = components['schemas']['Geschichte']; type GeschichteView = components['schemas']['GeschichteView'];
type Person = components['schemas']['Person']; type Person = components['schemas']['Person'];
type PersonOption = Pick<Person, 'id' | 'displayName'>;
interface Props { interface Props {
geschichte?: Geschichte | null; geschichte?: GeschichteView | null;
initialPersons?: Person[]; initialPersons?: Person[];
onSubmit: (payload: { onSubmit: (payload: {
title: string; title: string;
@@ -31,8 +32,13 @@ let { geschichte = null, initialPersons = [], onSubmit, submitting = false }: Pr
let title = $state(geschichte?.title ?? ''); let title = $state(geschichte?.title ?? '');
let body = $state(geschichte?.body ?? ''); let body = $state(geschichte?.body ?? '');
let status: 'DRAFT' | 'PUBLISHED' = $state(geschichte?.status ?? 'DRAFT'); let status: 'DRAFT' | 'PUBLISHED' = $state(geschichte?.status ?? 'DRAFT');
let selectedPersons: Person[] = $state( let selectedPersons: PersonOption[] = $state(
geschichte?.persons ? Array.from(geschichte.persons) : initialPersons geschichte?.persons
? Array.from(geschichte.persons).map((p) => ({
id: p.id,
displayName: [p.firstName, p.lastName].filter(Boolean).join(' ')
}))
: initialPersons
); );
let dirty = $state(false); let dirty = $state(false);

View File

@@ -4,10 +4,11 @@ import type { components } from '$lib/generated/api';
import PersonMultiSelect from '$lib/person/PersonMultiSelect.svelte'; import PersonMultiSelect from '$lib/person/PersonMultiSelect.svelte';
type Person = components['schemas']['Person']; type Person = components['schemas']['Person'];
type PersonOption = Pick<Person, 'id' | 'displayName'>;
interface Props { interface Props {
status: 'DRAFT' | 'PUBLISHED'; status: 'DRAFT' | 'PUBLISHED';
selectedPersons: Person[]; selectedPersons: PersonOption[];
} }
let { status, selectedPersons = $bindable() }: Props = $props(); let { status, selectedPersons = $bindable() }: Props = $props();

View File

@@ -3,9 +3,12 @@ import type { components } from '$lib/generated/api';
import { m } from '$lib/paraglide/messages.js'; import { m } from '$lib/paraglide/messages.js';
import { clickOutside } from '$lib/shared/actions/clickOutside'; import { clickOutside } from '$lib/shared/actions/clickOutside';
type Person = components['schemas']['Person']; 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<Person, 'id' | 'displayName'>;
interface Props { interface Props {
selectedPersons?: Person[]; selectedPersons?: PersonOption[];
} }
let { selectedPersons = $bindable([]) }: Props = $props(); let { selectedPersons = $bindable([]) }: Props = $props();

View File

@@ -3,10 +3,10 @@ import * as m from '$lib/paraglide/messages.js';
import { relativeTimeDe } from '$lib/shared/relativeTime'; import { relativeTimeDe } from '$lib/shared/relativeTime';
import type { components } from '$lib/generated/api'; import type { components } from '$lib/generated/api';
type Geschichte = components['schemas']['Geschichte']; type GeschichteSummary = components['schemas']['GeschichteSummary'];
interface Props { interface Props {
drafts: Geschichte[]; drafts: GeschichteSummary[];
} }
const { drafts }: Props = $props(); const { drafts }: Props = $props();

View File

@@ -5,24 +5,25 @@ import { page } from 'vitest/browser';
import ReaderDraftsModule from './ReaderDraftsModule.svelte'; import ReaderDraftsModule from './ReaderDraftsModule.svelte';
import type { components } from '$lib/generated/api'; import type { components } from '$lib/generated/api';
type Geschichte = components['schemas']['Geschichte']; type GeschichteSummary = components['schemas']['GeschichteSummary'];
afterEach(() => { afterEach(() => {
cleanup(); cleanup();
}); });
const draft1: Geschichte = { const draft1: GeschichteSummary = {
id: 'g1', id: 'g1',
title: 'Mein erster Entwurf', title: 'Mein erster Entwurf',
status: 'DRAFT', status: 'DRAFT',
createdAt: '2025-01-01T00:00:00Z', type: 'STORY',
updatedAt: '2025-01-02T00:00:00Z' updatedAt: '2025-01-02T00:00:00Z'
}; };
const draft2: Geschichte = { const draft2: GeschichteSummary = {
id: 'g2', id: 'g2',
title: 'Zweiter Entwurf', title: 'Zweiter Entwurf',
status: 'DRAFT', status: 'DRAFT',
type: 'STORY',
createdAt: '2025-02-01T00:00:00Z', createdAt: '2025-02-01T00:00:00Z',
updatedAt: '2025-02-01T00:00:00Z' updatedAt: '2025-02-01T00:00:00Z'
}; };

View File

@@ -3,10 +3,10 @@ import * as m from '$lib/paraglide/messages.js';
import { relativeTimeDe } from '$lib/shared/relativeTime'; import { relativeTimeDe } from '$lib/shared/relativeTime';
import type { components } from '$lib/generated/api'; import type { components } from '$lib/generated/api';
type Geschichte = components['schemas']['Geschichte']; type GeschichteSummary = components['schemas']['GeschichteSummary'];
interface Props { interface Props {
stories: Geschichte[]; stories: GeschichteSummary[];
} }
const { stories }: Props = $props(); const { stories }: Props = $props();

View File

@@ -5,27 +5,28 @@ import { page } from 'vitest/browser';
import ReaderRecentStories from './ReaderRecentStories.svelte'; import ReaderRecentStories from './ReaderRecentStories.svelte';
import type { components } from '$lib/generated/api'; import type { components } from '$lib/generated/api';
type Geschichte = components['schemas']['Geschichte']; type GeschichteSummary = components['schemas']['GeschichteSummary'];
afterEach(() => { afterEach(() => {
cleanup(); cleanup();
}); });
const story1: Geschichte = { const story1: GeschichteSummary = {
id: 'g1', id: 'g1',
title: 'Die Familie Müller', title: 'Die Familie Müller',
body: '<p>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.</p>', body: '<p>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.</p>',
status: 'PUBLISHED', status: 'PUBLISHED',
createdAt: '2025-01-01T00:00:00Z', type: 'STORY',
updatedAt: '2025-01-01T00:00:00Z', updatedAt: '2025-01-01T00:00:00Z',
publishedAt: '2025-01-01T00:00:00Z' publishedAt: '2025-01-01T00:00:00Z'
}; };
const longBodyStory: Geschichte = { const longBodyStory: GeschichteSummary = {
id: 'g2', id: 'g2',
title: 'Sehr lange Geschichte', title: 'Sehr lange Geschichte',
body: '<p>' + 'A'.repeat(200) + '</p>', body: '<p>' + 'A'.repeat(200) + '</p>',
status: 'PUBLISHED', status: 'PUBLISHED',
type: 'STORY',
createdAt: '2025-02-01T00:00:00Z', createdAt: '2025-02-01T00:00:00Z',
updatedAt: '2025-02-01T00:00:00Z', updatedAt: '2025-02-01T00:00:00Z',
publishedAt: '2025-02-01T00:00:00Z' publishedAt: '2025-02-01T00:00:00Z'

View File

@@ -11,7 +11,7 @@ type ActivityFeedItemDTO = components['schemas']['ActivityFeedItemDTO'];
type IncompleteDocumentDTO = components['schemas']['IncompleteDocumentDTO']; type IncompleteDocumentDTO = components['schemas']['IncompleteDocumentDTO'];
type PersonSummaryDTO = components['schemas']['PersonSummaryDTO']; type PersonSummaryDTO = components['schemas']['PersonSummaryDTO'];
type DocumentListItem = components['schemas']['DocumentListItem']; type DocumentListItem = components['schemas']['DocumentListItem'];
type Geschichte = components['schemas']['Geschichte']; type GeschichteSummary = components['schemas']['GeschichteSummary'];
type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO']; type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO'];
function settled<T>(res: PromiseSettledResult<unknown> | undefined): T | null { function settled<T>(res: PromiseSettledResult<unknown> | undefined): T | null {
@@ -57,9 +57,9 @@ export async function load({ fetch, parent }) {
const topPersons = settled<{ items: PersonSummaryDTO[] }>(topPersonsRes)?.items ?? []; const topPersons = settled<{ items: PersonSummaryDTO[] }>(topPersonsRes)?.items ?? [];
const searchData = settled<{ items: DocumentListItem[] }>(recentDocsRes); const searchData = settled<{ items: DocumentListItem[] }>(recentDocsRes);
const recentDocs = searchData?.items ?? []; const recentDocs = searchData?.items ?? [];
const recentStories = settled<Geschichte[]>(recentStoriesRes) ?? []; const recentStories = settled<GeschichteSummary[]>(recentStoriesRes) ?? [];
const tagTree = settled<TagTreeNodeDTO[]>(tagTreeRes) ?? []; const tagTree = settled<TagTreeNodeDTO[]>(tagTreeRes) ?? [];
const drafts = settled<Geschichte[]>(draftsRes) ?? []; const drafts = settled<GeschichteSummary[]>(draftsRes) ?? [];
return { return {
isReader: true as const, isReader: true as const,
@@ -179,9 +179,9 @@ export async function load({ fetch, parent }) {
readerStats: null, readerStats: null,
topPersons: [] as PersonSummaryDTO[], topPersons: [] as PersonSummaryDTO[],
recentDocs: [] as DocumentListItem[], recentDocs: [] as DocumentListItem[],
recentStories: [] as Geschichte[], recentStories: [] as GeschichteSummary[],
tagTree: [] as TagTreeNodeDTO[], tagTree: [] as TagTreeNodeDTO[],
drafts: [] as Geschichte[], drafts: [] as GeschichteSummary[],
error: 'Daten konnten nicht geladen werden.' as string | null error: 'Daten konnten nicht geladen werden.' as string | null
}; };
} }