fix: resolve all 47 svelte-check errors and 10 a11y warnings

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>
This commit is contained in:
Marcel
2026-03-16 12:08:25 +01:00
parent 5921a10d2e
commit 8a9e7bd9eb
17 changed files with 116 additions and 88 deletions

View File

@@ -80,6 +80,7 @@
<button
type="button"
on:click={() => removeTag(i)}
aria-label="Schlagwort entfernen"
class="text-brand-navy/50 hover:text-red-500 focus:outline-none"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"
@@ -117,12 +118,15 @@
class="absolute left-0 top-full mt-1 w-full bg-white border border-gray-200 rounded shadow-lg z-50 max-h-48 overflow-y-auto"
>
{#each suggestions as suggestion, i}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<li
role="option"
aria-selected={i === activeIndex}
tabindex="0"
class="px-3 py-2 text-sm cursor-pointer hover:bg-brand-sand/20 {i === activeIndex
? 'bg-brand-sand/20 text-brand-navy font-bold'
: 'text-gray-700'}"
on:click={() => addTag(suggestion)}
on:keydown={(e) => e.key === 'Enter' && addTag(suggestion)}
>
{suggestion}
</li>

View File

@@ -282,14 +282,14 @@ export interface components {
schemas: {
Tag: {
/** Format: uuid */
id?: string;
name?: string;
id: string;
name: string;
};
Person: {
/** Format: uuid */
id?: string;
firstName?: string;
lastName?: string;
id: string;
firstName: string;
lastName: string;
alias?: string;
};
DocumentUpdateDTO: {
@@ -307,13 +307,13 @@ export interface components {
};
Document: {
/** Format: uuid */
id?: string;
title?: string;
id: string;
title: string;
filePath?: string;
contentType?: string;
originalFilename?: string;
originalFilename: string;
/** @enum {string} */
status?: "PLACEHOLDER" | "UPLOADED" | "TRANSCRIBED" | "REVIEWED" | "ARCHIVED";
status: "PLACEHOLDER" | "UPLOADED" | "TRANSCRIBED" | "REVIEWED" | "ARCHIVED";
/** Format: date */
documentDate?: string;
location?: string;
@@ -323,9 +323,9 @@ export interface components {
transcription?: string;
summary?: string;
/** Format: date-time */
createdAt?: string;
createdAt: string;
/** Format: date-time */
updatedAt?: string;
updatedAt: string;
receivers?: components["schemas"]["Person"][];
sender?: components["schemas"]["Person"];
tags?: components["schemas"]["Tag"][];
@@ -338,20 +338,20 @@ export interface components {
};
AppUser: {
/** Format: uuid */
id?: string;
username?: string;
id: string;
username: string;
password?: string;
email?: string;
enabled?: boolean;
groups?: components["schemas"]["UserGroup"][];
enabled: boolean;
groups: components["schemas"]["UserGroup"][];
/** Format: date-time */
createdAt?: string;
createdAt: string;
};
UserGroup: {
/** Format: uuid */
id?: string;
name?: string;
permissions?: string[];
id: string;
name: string;
permissions: string[];
};
GroupDTO: {
name?: string;
@@ -738,7 +738,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
"*/*": string[];
"*/*": components["schemas"]["Tag"][];
};
};
};

View File

@@ -44,7 +44,8 @@ export async function load({ url, fetch }) {
senderName: senderObj ? `${senderObj.firstName} ${senderObj.lastName}` : '',
receiverName: receiverObj ? `${receiverObj.firstName} ${receiverObj.lastName}` : ''
},
filters: { q, from, to, senderId, receiverId, tags }
filters: { q, from, to, senderId, receiverId, tags },
error: null as string | null
};
} catch (e) {
if ((e as { status?: number }).status) throw e;
@@ -52,7 +53,8 @@ export async function load({ url, fetch }) {
return {
documents: [],
initialValues: { senderName: '', receiverName: '' },
filters: { q, from, to, senderId, receiverId, tags }
filters: { q, from, to, senderId, receiverId, tags },
error: 'Daten konnten nicht geladen werden.' as string | null
};
}
}

View File

@@ -143,9 +143,7 @@
>
<!-- Tag Filter -->
<div class="md:col-span-12">
<label class="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-2"
>Schlagworte</label
>
<p class="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-2">Schlagworte</p>
<TagInput bind:tags={tagNames} allowCreation={false} on:change={triggerSearch} />
</div>

View File

@@ -4,7 +4,7 @@ import { getErrorMessage } from '$lib/errors';
export async function load({ fetch, locals }) {
const user = locals.user;
const hasAdmin = user?.groups.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN'));
const hasAdmin = user?.groups?.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN'));
if (!hasAdmin) throw error(403, getErrorMessage('FORBIDDEN'));
const api = createApiClient(fetch);
@@ -27,17 +27,17 @@ export const actions = {
const data = await request.formData();
const api = createApiClient(fetch);
const { error: apiError, response } = await api.POST('/api/users', {
const result = await api.POST('/api/users', {
body: {
username: data.get('username'),
initialPassword: data.get('password'),
groupIds: data.getAll('groupIds')
username: data.get('username') as string,
initialPassword: data.get('password') as string,
groupIds: data.getAll('groupIds') as string[]
}
});
if (apiError) {
const code = (apiError as { code?: string })?.code;
return fail(response.status, { success: false, message: getErrorMessage(code) });
if (!result.response.ok) {
const code = (result.error as unknown as { code?: string })?.code;
return fail(result.response.status, { success: false, message: getErrorMessage(code) });
}
return { success: true };
},
@@ -47,13 +47,13 @@ export const actions = {
const id = data.get('id') as string;
const api = createApiClient(fetch);
const { error: apiError, response } = await api.DELETE('/api/users/{id}', {
const result = await api.DELETE('/api/users/{id}', {
params: { path: { id } }
});
if (apiError) {
const code = (apiError as { code?: string })?.code;
return fail(response.status, { success: false, message: getErrorMessage(code) });
if (!result.response.ok) {
const code = (result.error as unknown as { code?: string })?.code;
return fail(result.response.status, { success: false, message: getErrorMessage(code) });
}
return { success: true };
},
@@ -63,14 +63,14 @@ export const actions = {
const id = data.get('id') as string;
const api = createApiClient(fetch);
const { error: apiError, response } = await api.PUT('/api/tags/{id}', {
const result = await api.PUT('/api/tags/{id}', {
params: { path: { id } },
body: { name: data.get('name') }
body: { name: data.get('name') as string }
});
if (apiError) {
const code = (apiError as { code?: string })?.code;
return fail(response.status, { success: false, message: getErrorMessage(code) });
if (!result.response.ok) {
const code = (result.error as unknown as { code?: string })?.code;
return fail(result.response.status, { success: false, message: getErrorMessage(code) });
}
return { success: true };
},
@@ -80,13 +80,13 @@ export const actions = {
const id = data.get('id') as string;
const api = createApiClient(fetch);
const { error: apiError, response } = await api.DELETE('/api/tags/{id}', {
const result = await api.DELETE('/api/tags/{id}', {
params: { path: { id } }
});
if (apiError) {
const code = (apiError as { code?: string })?.code;
return fail(response.status, { success: false, message: getErrorMessage(code) });
if (!result.response.ok) {
const code = (result.error as unknown as { code?: string })?.code;
return fail(result.response.status, { success: false, message: getErrorMessage(code) });
}
return { success: true };
},
@@ -95,16 +95,16 @@ export const actions = {
const data = await request.formData();
const api = createApiClient(fetch);
const { error: apiError, response } = await api.POST('/api/groups', {
const result = await api.POST('/api/groups', {
body: {
name: data.get('name'),
permissions: data.getAll('permissions')
name: data.get('name') as string,
permissions: data.getAll('permissions') as string[]
}
});
if (apiError) {
const code = (apiError as { code?: string })?.code;
return fail(response.status, { success: false, message: getErrorMessage(code) });
if (!result.response.ok) {
const code = (result.error as unknown as { code?: string })?.code;
return fail(result.response.status, { success: false, message: getErrorMessage(code) });
}
return { success: true };
},
@@ -114,17 +114,17 @@ export const actions = {
const id = data.get('id') as string;
const api = createApiClient(fetch);
const { error: apiError, response } = await api.PATCH('/api/groups/{id}', {
const result = await api.PATCH('/api/groups/{id}', {
params: { path: { id } },
body: {
name: data.get('name'),
permissions: data.getAll('permissions')
name: data.get('name') as string,
permissions: data.getAll('permissions') as string[]
}
});
if (apiError) {
const code = (apiError as { code?: string })?.code;
return fail(response.status, { success: false, message: getErrorMessage(code) });
if (!result.response.ok) {
const code = (result.error as unknown as { code?: string })?.code;
return fail(result.response.status, { success: false, message: getErrorMessage(code) });
}
return { success: true };
},
@@ -134,13 +134,13 @@ export const actions = {
const id = data.get('id') as string;
const api = createApiClient(fetch);
const { error: apiError, response } = await api.DELETE('/api/groups/{id}', {
const result = await api.DELETE('/api/groups/{id}', {
params: { path: { id } }
});
if (apiError) {
const code = (apiError as { code?: string })?.code;
return fail(response.status, { success: false, message: getErrorMessage(code) });
if (!result.response.ok) {
const code = (result.error as unknown as { code?: string })?.code;
return fail(result.response.status, { success: false, message: getErrorMessage(code) });
}
return { success: true };
}

View File

@@ -319,9 +319,8 @@
name="name"
bind:value={editingTagName}
class="flex-1 border-brand-mint ring-1 ring-brand-mint rounded px-2 py-1 text-sm"
autofocus
/>
<button class="text-green-600 hover:text-green-800"
<button aria-label="Speichern" class="text-green-600 hover:text-green-800"
><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
stroke-linecap="round"
@@ -334,7 +333,8 @@
<button
type="button"
on:click={cancelEditTag}
class="text-gray-400 hover:text-gray-600"
aria-label="Abbrechen"
class="text-gray-400 hover:text-gray-600"
><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
stroke-linecap="round"
@@ -354,6 +354,7 @@
>
<button
on:click={() => startEditTag(tag)}
aria-label="Schlagwort bearbeiten"
class="p-1 text-gray-400 hover:text-brand-navy"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
@@ -386,7 +387,7 @@
class="inline"
>
<input type="hidden" name="id" value={tag.id} />
<button class="p-1 text-gray-400 hover:text-red-600">
<button aria-label="Schlagwort löschen" class="p-1 text-gray-400 hover:text-red-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
stroke-linecap="round"
@@ -473,7 +474,7 @@
<!-- Actions -->
<div class="flex gap-2 self-start sm:self-center">
<button type="submit" class="text-green-600 hover:text-green-800 p-1">
<button type="submit" aria-label="Speichern" class="text-green-600 hover:text-green-800 p-1">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
stroke-linecap="round"
@@ -486,7 +487,8 @@
<button
type="button"
on:click={cancelEditGroup}
class="text-gray-400 hover:text-red-500 p-1"
aria-label="Abbrechen"
class="text-gray-400 hover:text-red-500 p-1"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path

View File

@@ -1,3 +1,4 @@
import type { components } from '$lib/generated/api';
import { createApiClient } from '$lib/api.server';
export async function load({ url, fetch }) {
@@ -9,7 +10,7 @@ export async function load({ url, fetch }) {
const api = createApiClient(fetch);
let documents: unknown[] = [];
let documents: components['schemas']['Document'][] = [];
let senderName = '';
let receiverName = '';

View File

@@ -5,7 +5,7 @@
export let data;
// Data & State
let documents = [];
let documents: typeof data.documents = [];
let initialValues = { senderName: '', receiverName: '' };
// Filter State

View File

@@ -6,16 +6,14 @@ export async function load({ params, fetch }) {
const { id } = params;
const api = createApiClient(fetch);
const { data, error: apiError, response } = await api.GET('/api/documents/{id}', {
params: { path: { id } }
});
const result = await api.GET('/api/documents/{id}', { params: { path: { id } } });
if (response.status === 401) throw redirect(302, '/login');
if (result.response.status === 401) throw redirect(302, '/login');
if (apiError) {
const code = (apiError as { code?: string })?.code;
throw error(response.status, getErrorMessage(code));
if (!result.response.ok) {
const code = (result.error as unknown as { code?: string })?.code;
throw error(result.response.status, getErrorMessage(code));
}
return { document: data };
return { document: result.data! };
}

View File

@@ -12,7 +12,7 @@
loadFile(doc.id);
}
async function loadFile(id) {
async function loadFile(id: string) {
isLoading = true;
error = '';
fileUrl = ''; // Reset previous URL
@@ -430,7 +430,6 @@
src={fileUrl}
title="Document Preview"
class="w-full h-full border-none bg-white"
type="application/pdf"
></iframe>
{:else if fileUrl}
<!-- Image with Blob URL -->

View File

@@ -12,16 +12,16 @@ export async function load({ params, fetch }) {
api.GET('/api/persons')
]);
if (docResult.error) {
const code = (docResult.error as { code?: string })?.code;
if (!docResult.response.ok) {
const code = (docResult.error as unknown as { code?: string })?.code;
throw error(docResult.response.status, getErrorMessage(code));
}
if (personsResult.error) {
if (!personsResult.response.ok) {
throw error(personsResult.response.status, getErrorMessage('INTERNAL_ERROR'));
}
return {
document: docResult.data,
document: docResult.data!,
persons: personsResult.data
};
}

View File

@@ -11,13 +11,13 @@ export async function load({ params, fetch }) {
api.GET('/api/persons/{id}/documents', { params: { path: { id } } })
]);
if (personResult.error) {
const code = (personResult.error as { code?: string })?.code;
if (!personResult.response.ok) {
const code = (personResult.error as unknown as { code?: string })?.code;
throw error(personResult.response.status, getErrorMessage(code));
}
return {
person: personResult.data,
person: personResult.data!,
documents: docsResult.data ?? []
};
}