feat(observability): redesign +error.svelte with errorId display and copy button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-17 09:44:32 +02:00
parent dff81f7bfb
commit 96ea7e6815
5 changed files with 102 additions and 92 deletions

View File

@@ -473,7 +473,7 @@
"dashboard_reader_stats_persons_short": "Pers.",
"dashboard_reader_stats_stories_short": "Gesch.",
"dashboard_reader_draft_meta": "Entwurf · zuletzt bearbeitet {relative}",
"dashboard_resume_label": "Zuletzt geöffnet:",
"dashboard_resume_label": "Weiter, wo du aufgehört hast",
"dashboard_resume_fallback": "Unbekanntes Dokument",
"doc_status_placeholder": "Platzhalter",
"doc_status_uploaded": "Hochgeladen",
@@ -773,19 +773,15 @@
"admin_invite_created_title": "Einladung erstellt",
"admin_invite_created_desc": "Teile diesen Link mit der einzuladenden Person:",
"admin_invite_revoke_confirm": "Einladung wirklich widerrufen?",
"greeting_morning": "Guten Morgen, {name}.",
"greeting_day": "Hallo, {name}.",
"greeting_evening": "Guten Abend, {name}.",
"dashboard_resume_label": "Weiter, wo du aufgehört hast",
"dashboard_blocks": "{count} Abschnitte",
"dashboard_resume_cta": "Weitertranskribieren",
"dashboard_resume_other": "oder anderen Brief wählen",
"dashboard_empty_title": "Noch kein Dokument begonnen",
"dashboard_empty_body": "Wähle ein Dokument aus dem Archiv, um mit der Transkription zu beginnen.",
"dashboard_empty_cta": "Zum Archiv",
"dashboard_mission_caption": "Offene Aufgaben",
"queue_segment": "Segmentieren",
"queue_segment_blurb": "Seiten aufteilen",
@@ -795,7 +791,6 @@
"queue_review_blurb": "Texte kontrollieren",
"queue_n_open": "{n} offen",
"queue_show_all": "Alle anzeigen →",
"pulse_eyebrow": "Diese Woche",
"pulse_headline": "Ihr habt {pages} Seiten bearbeitet.",
"pulse_you": "Du selbst hast {pages} davon bearbeitet.",
@@ -803,19 +798,15 @@
"pulse_transcribed": "Textstellen markiert",
"pulse_reviewed": "Textstellen transkribiert",
"pulse_uploaded": "Dokumente hochgeladen",
"feed_caption": "Kommentare & Aktivität",
"feed_show_all": "Alle anzeigen",
"feed_for_you": "für dich",
"audit_action_text_saved": "hat Text gespeichert in",
"audit_action_file_uploaded": "hat eine Datei hochgeladen:",
"audit_action_annotation_created": "hat eine Markierung erstellt in",
"audit_action_comment_added": "hat kommentiert:",
"audit_action_mention_created": "hat dich erwähnt in",
"dropzone_release": "Loslassen zum Hochladen",
"chronik_page_title": "Aktivitäten",
"chronik_for_you_caption": "Für dich",
"chronik_for_you_count": "{count} neu",
@@ -859,9 +850,7 @@
"pagination_page_of": "Seite {page} von {total}",
"pagination_nav_label": "Seitennavigation",
"pagination_page_button": "Seite {page}",
"common_opens_new_tab": "(öffnet in neuem Tab)",
"transcribe_coach_title": "Erste Transkription?",
"transcribe_coach_preamble": "Unser Kurrent-Erkenner lernt noch. Jede Transkription, die Sie zum Training freigeben, bringt ihm die Schrift bei — so funktioniert's:",
"transcribe_coach_step_1_title": "Rahmen ziehen.",
@@ -871,10 +860,8 @@
"transcribe_coach_step_3_title": "Speichert automatisch.",
"transcribe_coach_footer_kurrent": "Hilfe zu Kurrent ↗",
"transcribe_coach_footer_richtlinien": "Transkriptions-Richtlinien ↗",
"transcription_mode_help_label": "Lese- und Bearbeitungsmodus",
"transcription_mode_help_body": "Lesen zeigt die Transkription als fließenden Text. Bearbeiten öffnet die Textfelder für jede Passage.",
"richtlinien_title": "Transkriptions-Richtlinien",
"richtlinien_intro": "Damit alle Briefe einheitlich transkribiert werden — egal wer tippt — hier unsere Regeln. Die Seite wächst mit: sobald wir eine neue Konvention beschließen, landet sie hier.",
"richtlinien_wiki_text": "Kurrent- und Sütterlin-Alphabete sind bei Wikipedia gut erklärt. Hier stehen nur unsere eigenen Vereinbarungen für dieses Archiv.",
@@ -948,12 +935,9 @@
"bulk_edit_all_x_failed": "Filter konnte nicht abgerufen werden — bitte erneut versuchen.",
"bulk_edit_topbar_title": "Massenbearbeitung",
"bulk_edit_count_pill": "{count} werden bearbeitet",
"nav_stammbaum": "Stammbaum",
"nav_geschichten": "Geschichten",
"error_geschichte_not_found": "Die Geschichte wurde nicht gefunden.",
"geschichten_index_title": "Geschichten",
"geschichten_new_button": "Neue Geschichte",
"geschichten_filter_all_pill": "Alle",
@@ -973,7 +957,6 @@
"geschichten_card_attach_action": "+ Geschichte anhängen",
"geschichten_card_show_all_for_person": "Alle Geschichten zu {name}",
"geschichten_card_show_all": "Alle anzeigen",
"geschichte_editor_title_placeholder": "Titel der Geschichte",
"geschichte_editor_body_placeholder": "Schreibe hier deine Geschichte…",
"geschichte_editor_status_draft": "ENTWURF",
@@ -1000,14 +983,11 @@
"geschichte_editor_toolbar_h3": "Unterüberschrift",
"geschichte_editor_toolbar_ul": "Aufzählung",
"geschichte_editor_toolbar_ol": "Nummerierte Liste",
"geschichte_delete_confirm_title": "Geschichte löschen?",
"geschichte_delete_confirm_body": "Diese Aktion kann nicht rückgängig gemacht werden. Die Geschichte wird dauerhaft gelöscht und aus allen verlinkten Personen- und Dokumentseiten entfernt.",
"error_relationship_not_found": "Die Beziehung wurde nicht gefunden.",
"error_circular_relationship": "Diese Beziehung würde einen Kreis erzeugen.",
"error_duplicate_relationship": "Diese Beziehung gibt es bereits.",
"relation_parent_of": "Elternteil von",
"relation_child_of": "Kind von",
"relation_spouse_of": "Ehegatte",
@@ -1018,7 +998,6 @@
"relation_doctor": "Arzt",
"relation_neighbor": "Nachbar",
"relation_other": "Sonstige",
"relation_inferred_parent": "Elternteil",
"relation_inferred_child": "Kind",
"relation_inferred_spouse": "Ehegatte",
@@ -1036,9 +1015,7 @@
"relation_inferred_sibling_inlaw": "Schwager/Schwägerin",
"relation_inferred_cousin_1": "Cousin/Cousine",
"relation_inferred_distant": "Weitläufige Verwandtschaft",
"doc_details_field_relationship": "Verwandtschaft",
"stammbaum_empty_heading": "Noch keine Familienmitglieder",
"stammbaum_empty_body": "Markiere Personen auf ihrer Bearbeitungsseite als Familienmitglied, damit sie hier erscheinen.",
"stammbaum_empty_link": "→ Zur Personenliste",
@@ -1050,7 +1027,6 @@
"stammbaum_zoom_in": "Vergrößern",
"stammbaum_zoom_out": "Verkleinern",
"stammbaum_generations": "Generationen",
"relation_error_duplicate": "Diese Beziehung gibt es bereits.",
"relation_error_circular": "Diese Beziehung würde einen Kreis erzeugen.",
"relation_error_self": "Eine Person kann nicht mit sich selbst verbunden werden.",
@@ -1073,14 +1049,15 @@
"relation_form_field_from_year": "Von Jahr",
"relation_form_field_to_year": "Bis Jahr",
"relation_form_year_placeholder": "z.B. 1920",
"person_relationships_heading": "Beziehungen",
"person_relationships_empty": "Noch keine Beziehungen bekannt.",
"timeline_aria_label": "Zeitachse Dokumentdichte",
"timeline_clear_selection": "Auswahl zurücksetzen",
"timeline_zoom_reset": "Zurück zur Übersicht",
"timeline_bar_aria_singular": "{when}, 1 Dokument",
"timeline_bar_aria_plural": "{when}, {count} Dokumente",
"timeline_dragging_aria_live": "Zeitraum {from} bis {to} ausgewählt"
"timeline_dragging_aria_live": "Zeitraum {from} bis {to} ausgewählt",
"error_page_id_label": "Fehler-ID",
"error_copy_id_label": "ID kopieren",
"error_copied": "Kopiert!"
}

View File

@@ -473,7 +473,7 @@
"dashboard_reader_stats_persons_short": "Pers.",
"dashboard_reader_stats_stories_short": "Stor.",
"dashboard_reader_draft_meta": "Draft · last edited {relative}",
"dashboard_resume_label": "Last opened:",
"dashboard_resume_label": "Continue where you left off",
"dashboard_resume_fallback": "Unknown document",
"doc_status_placeholder": "Placeholder",
"doc_status_uploaded": "Uploaded",
@@ -773,19 +773,15 @@
"admin_invite_created_title": "Invite created",
"admin_invite_created_desc": "Share this link with the person you are inviting:",
"admin_invite_revoke_confirm": "Really revoke this invite?",
"greeting_morning": "Good morning, {name}.",
"greeting_day": "Hello, {name}.",
"greeting_evening": "Good evening, {name}.",
"dashboard_resume_label": "Continue where you left off",
"dashboard_blocks": "{count} sections",
"dashboard_resume_cta": "Continue transcribing",
"dashboard_resume_other": "or choose another document",
"dashboard_empty_title": "No document started yet",
"dashboard_empty_body": "Choose a document from the archive to start transcribing.",
"dashboard_empty_cta": "To the archive",
"dashboard_mission_caption": "Open tasks",
"queue_segment": "Segment",
"queue_segment_blurb": "Split pages",
@@ -795,7 +791,6 @@
"queue_review_blurb": "Check texts",
"queue_n_open": "{n} open",
"queue_show_all": "Show all →",
"pulse_eyebrow": "This week",
"pulse_headline": "You have worked on {pages} pages.",
"pulse_you": "You personally worked on {pages} of them.",
@@ -803,19 +798,15 @@
"pulse_transcribed": "Passages annotated",
"pulse_reviewed": "Passages transcribed",
"pulse_uploaded": "Documents uploaded",
"feed_caption": "Comments & activity",
"feed_show_all": "Show all",
"feed_for_you": "for you",
"audit_action_text_saved": "saved text in",
"audit_action_file_uploaded": "uploaded a file:",
"audit_action_annotation_created": "created an annotation in",
"audit_action_comment_added": "commented:",
"audit_action_mention_created": "mentioned you in",
"dropzone_release": "Release to upload",
"chronik_page_title": "Activity",
"chronik_for_you_caption": "For you",
"chronik_for_you_count": "{count} new",
@@ -859,9 +850,7 @@
"pagination_page_of": "Page {page} of {total}",
"pagination_nav_label": "Pagination",
"pagination_page_button": "Page {page}",
"common_opens_new_tab": "(opens in new tab)",
"transcribe_coach_title": "First transcription?",
"transcribe_coach_preamble": "Our Kurrent recogniser is still learning. Every transcription you release for training teaches it the handwriting — here's how it works:",
"transcribe_coach_step_1_title": "Draw a frame.",
@@ -871,10 +860,8 @@
"transcribe_coach_step_3_title": "Saves automatically.",
"transcribe_coach_footer_kurrent": "Kurrent help ↗",
"transcribe_coach_footer_richtlinien": "Transcription guidelines ↗",
"transcription_mode_help_label": "Read and edit mode",
"transcription_mode_help_body": "Read shows the transcription as flowing text. Edit opens the text fields for each passage.",
"richtlinien_title": "Transcription Guidelines",
"richtlinien_intro": "So every letter is transcribed consistently — no matter who types — here are our rules. The page grows with us: as soon as we agree a new convention, it lands here.",
"richtlinien_wiki_text": "The Kurrent and Sütterlin alphabets are well explained on Wikipedia. Here you'll only find our own conventions for this archive.",
@@ -948,12 +935,9 @@
"bulk_edit_all_x_failed": "Could not load filter results — please retry.",
"bulk_edit_topbar_title": "Bulk edit",
"bulk_edit_count_pill": "{count} will be edited",
"nav_stammbaum": "Family tree",
"nav_geschichten": "Stories",
"error_geschichte_not_found": "The story was not found.",
"geschichten_index_title": "Stories",
"geschichten_new_button": "New story",
"geschichten_filter_all_pill": "All",
@@ -973,7 +957,6 @@
"geschichten_card_attach_action": "+ Attach a story",
"geschichten_card_show_all_for_person": "All stories about {name}",
"geschichten_card_show_all": "Show all",
"geschichte_editor_title_placeholder": "Story title",
"geschichte_editor_body_placeholder": "Write your story here…",
"geschichte_editor_status_draft": "DRAFT",
@@ -1000,14 +983,11 @@
"geschichte_editor_toolbar_h3": "Subheading",
"geschichte_editor_toolbar_ul": "Bulleted list",
"geschichte_editor_toolbar_ol": "Numbered list",
"geschichte_delete_confirm_title": "Delete story?",
"geschichte_delete_confirm_body": "This action cannot be undone. The story will be permanently deleted and removed from all linked person and document pages.",
"error_relationship_not_found": "Relationship not found.",
"error_circular_relationship": "This relationship would form a cycle.",
"error_duplicate_relationship": "This relationship already exists.",
"relation_parent_of": "Parent of",
"relation_child_of": "Child of",
"relation_spouse_of": "Spouse",
@@ -1018,7 +998,6 @@
"relation_doctor": "Doctor",
"relation_neighbor": "Neighbour",
"relation_other": "Other",
"relation_inferred_parent": "Parent",
"relation_inferred_child": "Child",
"relation_inferred_spouse": "Spouse",
@@ -1036,9 +1015,7 @@
"relation_inferred_sibling_inlaw": "Sibling-in-law",
"relation_inferred_cousin_1": "Cousin",
"relation_inferred_distant": "Distant relative",
"doc_details_field_relationship": "Relationship",
"stammbaum_empty_heading": "No family members yet",
"stammbaum_empty_body": "Mark a person as a family member on their edit page so they appear here.",
"stammbaum_empty_link": "→ Go to person list",
@@ -1050,7 +1027,6 @@
"stammbaum_zoom_in": "Zoom in",
"stammbaum_zoom_out": "Zoom out",
"stammbaum_generations": "Generations",
"relation_error_duplicate": "This relationship already exists.",
"relation_error_circular": "This relationship would form a cycle.",
"relation_error_self": "A person cannot be related to themselves.",
@@ -1073,14 +1049,15 @@
"relation_form_field_from_year": "From year",
"relation_form_field_to_year": "To year",
"relation_form_year_placeholder": "e.g. 1920",
"person_relationships_heading": "Relationships",
"person_relationships_empty": "No relationships known yet.",
"timeline_aria_label": "Document density timeline",
"timeline_clear_selection": "Clear selection",
"timeline_zoom_reset": "Reset zoom",
"timeline_bar_aria_singular": "{when}, 1 document",
"timeline_bar_aria_plural": "{when}, {count} documents",
"timeline_dragging_aria_live": "Range {from} to {to} selected"
"timeline_dragging_aria_live": "Range {from} to {to} selected",
"error_page_id_label": "Error ID",
"error_copy_id_label": "Copy ID",
"error_copied": "Copied!"
}

View File

@@ -473,7 +473,7 @@
"dashboard_reader_stats_persons_short": "Pers.",
"dashboard_reader_stats_stories_short": "Hist.",
"dashboard_reader_draft_meta": "Borrador · editado hace {relative}",
"dashboard_resume_label": "Último abierto:",
"dashboard_resume_label": "Continuar donde lo dejaste",
"dashboard_resume_fallback": "Documento desconocido",
"doc_status_placeholder": "Marcador",
"doc_status_uploaded": "Cargado",
@@ -773,19 +773,15 @@
"admin_invite_created_title": "Invitación creada",
"admin_invite_created_desc": "Comparte este enlace con la persona invitada:",
"admin_invite_revoke_confirm": "¿Realmente revocar esta invitación?",
"greeting_morning": "Buenos días, {name}.",
"greeting_day": "Hola, {name}.",
"greeting_evening": "Buenas noches, {name}.",
"dashboard_resume_label": "Continuar donde lo dejaste",
"dashboard_blocks": "{count} secciones",
"dashboard_resume_cta": "Continuar transcripción",
"dashboard_resume_other": "o elige otro documento",
"dashboard_empty_title": "Aún no has comenzado ningún documento",
"dashboard_empty_body": "Elige un documento del archivo para empezar a transcribir.",
"dashboard_empty_cta": "Al archivo",
"dashboard_mission_caption": "Tareas pendientes",
"queue_segment": "Segmentar",
"queue_segment_blurb": "Dividir páginas",
@@ -795,7 +791,6 @@
"queue_review_blurb": "Controlar textos",
"queue_n_open": "{n} pendiente",
"queue_show_all": "Ver todo →",
"pulse_eyebrow": "Esta semana",
"pulse_headline": "Habéis trabajado {pages} páginas.",
"pulse_you": "Tú mismo has trabajado {pages} de ellas.",
@@ -803,19 +798,15 @@
"pulse_transcribed": "Fragmentos anotados",
"pulse_reviewed": "Fragmentos transcritos",
"pulse_uploaded": "Documentos subidos",
"feed_caption": "Comentarios y actividad",
"feed_show_all": "Ver todo",
"feed_for_you": "para ti",
"audit_action_text_saved": "guardó texto en",
"audit_action_file_uploaded": "subió un archivo:",
"audit_action_annotation_created": "creó una anotación en",
"audit_action_comment_added": "comentó:",
"audit_action_mention_created": "te mencionó en",
"dropzone_release": "Suelta para subir",
"chronik_page_title": "Actividades",
"chronik_for_you_caption": "Para ti",
"chronik_for_you_count": "{count} nuevas",
@@ -859,9 +850,7 @@
"pagination_page_of": "Página {page} de {total}",
"pagination_nav_label": "Paginación",
"pagination_page_button": "Página {page}",
"common_opens_new_tab": "(abre en pestaña nueva)",
"transcribe_coach_title": "¿Primera transcripción?",
"transcribe_coach_preamble": "Nuestro reconocedor de Kurrent aún está aprendiendo. Cada transcripción que libera para el entrenamiento le enseña la escritura — así funciona:",
"transcribe_coach_step_1_title": "Dibujar un marco.",
@@ -871,10 +860,8 @@
"transcribe_coach_step_3_title": "Se guarda automáticamente.",
"transcribe_coach_footer_kurrent": "Ayuda sobre Kurrent ↗",
"transcribe_coach_footer_richtlinien": "Normas de transcripción ↗",
"transcription_mode_help_label": "Modo lectura y edición",
"transcription_mode_help_body": "Lectura muestra la transcripción como texto continuo. Edición abre los campos de texto para cada pasaje.",
"richtlinien_title": "Normas de transcripción",
"richtlinien_intro": "Para que todas las cartas se transcriban de forma uniforme — sin importar quién transcriba — aquí están nuestras reglas. La página crece con nosotros.",
"richtlinien_wiki_text": "Los alfabetos Kurrent y Sütterlin están bien explicados en Wikipedia. Aquí solo se recogen nuestros propios acuerdos para este archivo.",
@@ -948,12 +935,9 @@
"bulk_edit_all_x_failed": "No se pudieron cargar los resultados del filtro; vuelve a intentarlo.",
"bulk_edit_topbar_title": "Edición masiva",
"bulk_edit_count_pill": "Se editarán {count}",
"nav_stammbaum": "Árbol genealógico",
"nav_geschichten": "Historias",
"error_geschichte_not_found": "No se encontró la historia.",
"geschichten_index_title": "Historias",
"geschichten_new_button": "Nueva historia",
"geschichten_filter_all_pill": "Todas",
@@ -973,7 +957,6 @@
"geschichten_card_attach_action": "+ Adjuntar historia",
"geschichten_card_show_all_for_person": "Todas las historias sobre {name}",
"geschichten_card_show_all": "Mostrar todas",
"geschichte_editor_title_placeholder": "Título de la historia",
"geschichte_editor_body_placeholder": "Escribe tu historia aquí…",
"geschichte_editor_status_draft": "BORRADOR",
@@ -1000,14 +983,11 @@
"geschichte_editor_toolbar_h3": "Subencabezado",
"geschichte_editor_toolbar_ul": "Lista con viñetas",
"geschichte_editor_toolbar_ol": "Lista numerada",
"geschichte_delete_confirm_title": "¿Eliminar historia?",
"geschichte_delete_confirm_body": "Esta acción no se puede deshacer. La historia se eliminará permanentemente y se quitará de todas las páginas de personas y documentos vinculados.",
"error_relationship_not_found": "La relación no fue encontrada.",
"error_circular_relationship": "Esta relación crearía un ciclo.",
"error_duplicate_relationship": "Esta relación ya existe.",
"relation_parent_of": "Progenitor de",
"relation_child_of": "Hijo/a de",
"relation_spouse_of": "Cónyuge",
@@ -1018,7 +998,6 @@
"relation_doctor": "Médico",
"relation_neighbor": "Vecino/a",
"relation_other": "Otro",
"relation_inferred_parent": "Progenitor",
"relation_inferred_child": "Hijo/a",
"relation_inferred_spouse": "Cónyuge",
@@ -1036,9 +1015,7 @@
"relation_inferred_sibling_inlaw": "Cuñado/a",
"relation_inferred_cousin_1": "Primo/a",
"relation_inferred_distant": "Pariente lejano",
"doc_details_field_relationship": "Parentesco",
"stammbaum_empty_heading": "Aún no hay miembros de la familia",
"stammbaum_empty_body": "Marca a una persona como miembro de la familia en su página de edición para que aparezca aquí.",
"stammbaum_empty_link": "→ Ir a la lista de personas",
@@ -1050,7 +1027,6 @@
"stammbaum_zoom_in": "Acercar",
"stammbaum_zoom_out": "Alejar",
"stammbaum_generations": "Generaciones",
"relation_error_duplicate": "Esta relación ya existe.",
"relation_error_circular": "Esta relación crearía un ciclo.",
"relation_error_self": "Una persona no puede estar relacionada consigo misma.",
@@ -1073,14 +1049,15 @@
"relation_form_field_from_year": "Desde año",
"relation_form_field_to_year": "Hasta año",
"relation_form_year_placeholder": "ej. 1920",
"person_relationships_heading": "Relaciones",
"person_relationships_empty": "Aún no se conocen relaciones.",
"timeline_aria_label": "Cronología de densidad de documentos",
"timeline_clear_selection": "Borrar selección",
"timeline_zoom_reset": "Restablecer zoom",
"timeline_bar_aria_singular": "{when}, 1 documento",
"timeline_bar_aria_plural": "{when}, {count} documentos",
"timeline_dragging_aria_live": "Rango {from} a {to} seleccionado"
"timeline_dragging_aria_live": "Rango {from} a {to} seleccionado",
"error_page_id_label": "ID de error",
"error_copy_id_label": "Copiar ID",
"error_copied": "¡Copiado!"
}

View File

@@ -1,13 +1,47 @@
<script lang="ts">
import { page } from '$app/state';
import { m } from '$lib/paraglide/messages.js';
let copied = $state(false);
function copyId() {
const id = page.error?.errorId;
if (!id) return;
navigator.clipboard.writeText(id).then(() => {
copied = true;
setTimeout(() => (copied = false), 2000);
});
}
</script>
<svelte:head>
<title>{m.page_title_error()}</title>
</svelte:head>
<div class="px-4 py-12 text-center font-sans">
<p class="font-sans text-6xl font-bold text-ink">{page.status}</p>
<p class="mt-2 font-sans text-sm text-ink-2">{page.error?.message ?? 'Internal Error'}</p>
</div>
<main class="px-4 py-12 text-center font-sans">
<h1 class="mb-2 font-serif text-2xl font-bold text-ink">{m.page_title_error()}</h1>
<p class="mb-8 font-sans text-sm text-ink-2">
{page.error?.message ?? m.error_internal_error()}
</p>
<p class="font-sans text-xs tracking-widest text-ink-3 uppercase">{page.status}</p>
{#if page.error?.errorId}
<div class="mt-6 flex flex-col items-center gap-3">
<p class="font-sans text-xs tracking-widest text-ink-2 uppercase">
{m.error_page_id_label()}
</p>
<code
class="rounded border border-line bg-surface px-3 py-1 font-mono text-sm text-ink select-all"
>
{page.error.errorId}
</code>
<button
class="min-h-[44px] rounded-sm bg-brand-navy px-5 py-2 font-sans text-sm text-white transition-colors hover:opacity-90 focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-offset-2"
onclick={copyId}
aria-label={m.error_copy_id_label()}
>
<span aria-live="polite">{copied ? m.error_copied() : m.error_copy_id_label()}</span>
</button>
</div>
{/if}
</main>

View File

@@ -4,7 +4,10 @@ import { page as browserPage } from 'vitest/browser';
const mockPage = {
status: 500,
error: { message: 'Internal Error' } as { message: string } | null
error: { message: 'Internal Error', errorId: undefined } as {
message: string;
errorId?: string;
} | null
};
vi.mock('$app/state', () => ({
@@ -13,6 +16,16 @@ vi.mock('$app/state', () => ({
}
}));
vi.mock('$lib/paraglide/messages.js', () => ({
m: {
page_title_error: () => 'Es ist etwas schiefgelaufen.',
error_internal_error: () => 'Ein unerwarteter Fehler ist aufgetreten.',
error_page_id_label: () => 'Fehler-ID',
error_copy_id_label: () => 'ID kopieren',
error_copied: () => 'Kopiert!'
}
}));
afterEach(cleanup);
async function loadComponent() {
@@ -20,7 +33,7 @@ async function loadComponent() {
}
describe('+error.svelte', () => {
it('renders the page status code prominently', async () => {
it('renders the page status code', async () => {
mockPage.status = 404;
mockPage.error = { message: 'Not Found' };
@@ -40,13 +53,45 @@ describe('+error.svelte', () => {
await expect.element(browserPage.getByText('Database unavailable')).toBeVisible();
});
it('falls back to the literal "Internal Error" when page.error is null', async () => {
it('falls back to error_internal_error message when page.error is null', async () => {
mockPage.status = 500;
mockPage.error = null;
const ErrorPage = await loadComponent();
render(ErrorPage);
await expect.element(browserPage.getByText('Internal Error')).toBeVisible();
await expect
.element(browserPage.getByText('Ein unerwarteter Fehler ist aufgetreten.'))
.toBeVisible();
});
it('shows errorId when page.error.errorId is set', async () => {
mockPage.status = 500;
mockPage.error = { message: 'Something broke', errorId: 'abc-123-def' };
const ErrorPage = await loadComponent();
render(ErrorPage);
await expect.element(browserPage.getByText('abc-123-def')).toBeVisible();
});
it('shows copy button when errorId is present', async () => {
mockPage.status = 500;
mockPage.error = { message: 'Something broke', errorId: 'abc-123-def' };
const ErrorPage = await loadComponent();
render(ErrorPage);
await expect.element(browserPage.getByRole('button', { name: 'ID kopieren' })).toBeVisible();
});
it('does not render errorId section when errorId is absent', async () => {
mockPage.status = 500;
mockPage.error = { message: 'Something broke' };
const ErrorPage = await loadComponent();
render(ErrorPage);
await expect.element(browserPage.getByText('Fehler-ID')).not.toBeInTheDocument();
});
});