Files
familienarchiv/docs/specs/ocr-admin-spec.html
Marcel 78eca8e9a1
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m36s
CI / OCR Service Tests (push) Successful in 27s
CI / Backend Unit Tests (push) Failing after 1m22s
docs(ocr): add Admin OCR overview & model-detail UI spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 19:11:07 +02:00

1102 lines
59 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin — OCR: Übersicht &amp; Modell-Detail</title>
<link href="https://fonts.googleapis.com/css2?family=Tinos:wght@400;700&family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Montserrat',system-ui,sans-serif;background:#E8E6DF;color:#1A1A24;line-height:1.5;font-size:13px}
.page{max-width:1400px;margin:0 auto;padding:48px 32px 120px}
/* ── Masthead ── */
.mh{padding-bottom:24px;border-bottom:3px solid #012851;margin-bottom:56px}
.mh h1{font-size:24px;font-weight:900;color:#012851;letter-spacing:-.4px}
.mh p{font-size:12.5px;color:#555;max-width:720px;line-height:1.75;margin-top:8px}
.mh .byline{font-size:9px;color:#999;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;margin-top:10px}
.tag-row{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
.tag{background:#012851;color:#a1dcd8;padding:2px 8px;border-radius:2px;font-size:8px;font-weight:700;letter-spacing:.8px;text-transform:uppercase}
.tag.amber{background:#92400e;color:#fef3c7}
.tag.green{background:#3a6e42;color:#d1fae5}
.tag.gray{background:#4b5563;color:#e5e7eb}
/* ── Section headers ── */
.sh{margin:64px 0 28px;padding-bottom:14px;border-bottom:2px solid #D8D5CE}
.sh h2{font-size:16px;font-weight:900;color:#012851}
.sh p{font-size:12px;color:#666;margin-top:4px;max-width:760px;line-height:1.65}
/* ── Grid layouts ── */
.grid{display:flex;gap:24px;flex-wrap:wrap;margin-bottom:32px;align-items:flex-start}
.col{display:flex;flex-direction:column;gap:8px}
.lbl{font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#888;display:flex;align-items:center;gap:5px}
.lbl .badge{background:#E0DDD6;color:#666;padding:1px 5px;border-radius:2px;font-size:7px;font-weight:700}
.lbl .badge.new{background:#012851;color:#a1dcd8}
.cap{font-size:10px;color:#888;font-style:italic;line-height:1.6;max-width:480px;margin-top:4px}
/* ── Browser chrome ── */
.chrome{background:#F5F4EE;border:1.5px solid #C4C0BA;border-radius:8px;overflow:hidden;box-shadow:0 4px 16px rgba(0,0,0,.1)}
.bar{height:18px;background:#E8E6E0;border-bottom:1px solid #C4C0BA;display:flex;align-items:center;padding:0 7px;gap:3px;flex-shrink:0}
.dot{width:5px;height:5px;border-radius:50%;background:#BDB8B1}
.url{flex:1;height:7px;background:#CCC8C2;border-radius:5px;margin-left:4px}
/* ── App header (50px real → 28px spec) ── */
.app-header{height:28px;background:#012851;display:flex;align-items:center;padding:0 10px;gap:8px;flex-shrink:0}
.app-logo{font-size:6.5px;font-weight:900;color:#fff;letter-spacing:.8px;border-bottom:2px solid #a1dcd8;padding-bottom:1px;font-family:'Montserrat',sans-serif;white-space:nowrap}
.app-link{font-size:5px;color:rgba(255,255,255,.4);font-weight:700;text-transform:uppercase;letter-spacing:.4px;white-space:nowrap}
.app-link.act{color:rgba(255,255,255,.85)}
.app-nav-r{margin-left:auto;display:flex;gap:4px;align-items:center}
.app-av{width:14px;height:14px;background:rgba(255,255,255,.12);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:5px;font-weight:800;color:rgba(255,255,255,.5)}
/* ── Admin shell ── */
.admin-shell{display:flex;overflow:hidden}
/* ── EntityNav ── */
.entity-nav{width:66px;flex-shrink:0;background:#012851;display:flex;flex-direction:column}
.en-heading{font-size:4.5px;font-weight:800;text-transform:uppercase;letter-spacing:1px;color:rgba(255,255,255,.35);padding:5px 6px 2px}
.en-item{display:flex;flex-direction:column;align-items:flex-start;gap:1px;padding:5px 6px 5px 5px;border-left:2.5px solid transparent;cursor:pointer}
.en-item:hover:not(.en-active){background:rgba(255,255,255,.05)}
.en-item.en-active{border-left-color:#a1dcd8;background:rgba(255,255,255,.08)}
.en-icon{width:9px;height:9px;border-radius:1px;background:rgba(255,255,255,.25);flex-shrink:0}
.en-icon.mint{background:#a1dcd8}
.en-count{font-size:5.5px;font-weight:900;color:rgba(255,255,255,.4);margin-top:1px}
.en-count.mint{color:rgba(255,255,255,.6)}
.en-label{font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:rgba(255,255,255,.45)}
.en-label.mint{color:rgba(255,255,255,.95)}
.en-spacer{flex:1}
.en-sep{border-top:1px solid rgba(255,255,255,.1)}
.v-line{width:1px;background:#e4e2d7;flex-shrink:0}
/* ── OCR content area ── */
.ocr-content{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0;background:#F5F4EE}
.ocr-page-header{display:flex;align-items:center;gap:5px;padding:5px 10px;border-bottom:1px solid #e4e2d7;background:white;flex-shrink:0}
.oph-back{font-size:5px;color:#9ca3af;cursor:pointer;flex-shrink:0}
.oph-title{font-size:7.5px;font-weight:800;color:#012851;font-family:'Montserrat',sans-serif}
.oph-sub{font-size:5px;color:#6b7280}
.ocr-scroll{flex:1;overflow-y:auto;padding:7px 9px;display:flex;flex-direction:column;gap:5px}
/* ── Status pills ── */
.pill{display:inline-flex;align-items:center;gap:2px;padding:1px 5px;border-radius:2px;font-size:5px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;flex-shrink:0}
.pill.running{background:#fff8e1;color:#f57c00;border:1px solid #ffe082}
.pill.done{background:#e8f5e9;color:#2e7d32;border:1px solid #c8e6c9}
.pill.failed{background:#ffcdd2;color:#c62828;border:1px solid #ef9a9a}
.pill.queued{background:#fef3c7;color:#92400e;border:1px solid #fde68a}
.pill.online{background:#e8f5e9;color:#2e7d32;border:1px solid #c8e6c9}
/* ── Health bar ── */
.health-bar{display:flex;align-items:center;gap:5px;padding:5px 9px;background:white;border:1px solid #e4e2d7;border-radius:2px}
.hb-spacer{flex:1}
.hb-sep{width:1px;height:10px;background:#e4e2d7;flex-shrink:0}
/* ── Stat cards ── */
.stat-row{display:grid;grid-template-columns:repeat(3,1fr);gap:5px}
.stat-card{background:white;border:1px solid #e4e2d7;border-radius:2px;padding:8px 10px}
.sc-num{font-family:'Tinos',serif;font-size:20px;font-weight:700;color:#012851;line-height:1}
.sc-lbl{font-size:5px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-top:3px}
.sc-sub{font-size:4.5px;color:#aaa;margin-top:1px}
/* ── Models table ── */
.models-table{background:white;border:1px solid #e4e2d7;border-radius:2px;overflow:hidden}
.mt-header-row{display:flex;align-items:center;padding:4px 9px;border-bottom:1px solid #e4e2d7}
.mt-section-lbl{font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.9px;color:#6b7280}
.mt-thead{background:#012851;padding:3px 9px;display:grid;grid-template-columns:2fr 52px 52px 55px 60px 68px;gap:4px}
.mt-th{font-size:4.5px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#a1dcd8}
.mt-row{padding:5px 9px;display:grid;grid-template-columns:2fr 52px 52px 55px 60px 68px;gap:4px;border-bottom:1px solid #f0efe9;cursor:pointer;align-items:center}
.mt-row:last-child{border-bottom:0}
.mt-row.global-row{background:#F8F8FC}
.mt-name{font-size:6px;font-weight:700;color:#012851}
.mt-name.sender{font-weight:500;color:#374151}
.mt-person{font-size:4.5px;color:#9ca3af;margin-top:1px}
.mt-val{font-size:6px;color:#374151}
.mt-val.good{color:#2e7d32;font-weight:600}
.mt-val.bad{color:#c62828;font-weight:600}
.mt-val.muted{color:#9ca3af}
/* ── Actions section ── */
.actions-section{background:white;border:1px solid #e4e2d7;border-radius:2px;padding:7px 9px}
.as-lbl{font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.9px;color:#6b7280;margin-bottom:5px}
.as-row{display:flex;gap:4px;flex-wrap:wrap}
.btn-primary{display:inline-flex;align-items:center;height:16px;padding:0 8px;background:#012851;color:white;border-radius:2px;font-size:5px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;cursor:pointer;white-space:nowrap}
.btn-outline{display:inline-flex;align-items:center;height:16px;padding:0 8px;border:1.5px solid #012851;color:#012851;border-radius:2px;font-size:5px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;cursor:pointer;white-space:nowrap}
.btn-link{font-size:5px;color:#012851;text-decoration:underline;cursor:pointer;align-self:center}
/* ── Section action bar ── */
.section-bar{display:flex;align-items:center;gap:4px}
.section-bar-lbl{font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.9px;color:#6b7280;flex:1}
/* ── Metric cards ── */
.metric-row{display:grid;grid-template-columns:repeat(4,1fr);gap:5px}
.metric-card{background:white;border:1px solid #e4e2d7;border-radius:2px;padding:8px 10px;text-align:center}
.mc-num{font-family:'Tinos',serif;font-size:18px;font-weight:700;line-height:1}
.mc-num.good{color:#2e7d32}
.mc-num.neutral{color:#012851}
.mc-lbl{font-size:5px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-top:3px}
.mc-delta{font-size:4.5px;margin-top:2px}
.mc-delta.up{color:#2e7d32}
.mc-delta.down{color:#c62828}
.mc-delta.dim{color:#9ca3af}
/* ── History table ── */
.hist-table{background:white;border:1px solid #e4e2d7;border-radius:2px;overflow:hidden}
.ht-thead{background:#012851;padding:3px 9px;display:grid;grid-template-columns:68px 1fr 55px 55px 55px 55px;gap:4px}
.ht-th{font-size:4.5px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#a1dcd8}
.ht-row{padding:5px 9px;display:grid;grid-template-columns:68px 1fr 55px 55px 55px 55px;gap:4px;border-bottom:1px solid #f0efe9;align-items:center}
.ht-row:last-child{border-bottom:0}
.ht-row.running{background:#fff8e1}
.ht-row.failed-row{background:#fff8f8}
.ht-val{font-size:6px;color:#374151}
.ht-val.good{color:#2e7d32;font-weight:600}
.ht-val.muted{color:#9ca3af}
.ht-err{font-size:4.5px;color:#c62828}
/* ── Phone frame ── */
.phone{width:212px;background:#F5F4EE;border-radius:22px;overflow:hidden;box-shadow:0 8px 24px rgba(0,0,0,.18);border:4px solid #1C1C24;display:flex;flex-direction:column}
.phone-status{height:13px;background:#F5F4EE;display:flex;align-items:center;justify-content:space-between;padding:0 12px;font-size:5.5px;font-weight:600;color:#6b7280;flex-shrink:0}
.phone-body{display:flex;flex-direction:column;overflow:hidden;flex:1}
.phone-scroll{flex:1;overflow-y:auto;background:#F5F4EE;padding:5px;display:flex;flex-direction:column;gap:4px}
/* ── Annotations ── */
.annotation{display:flex;gap:8px;margin-top:8px;align-items:flex-start}
.ann-bullet{width:16px;height:16px;border-radius:50%;background:#012851;color:#a1dcd8;font-size:7px;font-weight:800;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}
.ann-text{font-size:11px;color:#555;line-height:1.65;flex:1}
.ann-text strong{color:#1A1A24;font-weight:700}
.ann-text code{font-family:monospace;background:#E0DDD6;padding:1px 4px;border-radius:2px;font-size:10px;color:#012851}
/* ── Impl-ref table ── */
.impl-ref table{width:100%;border-collapse:collapse;font-size:11px;margin-top:12px}
.impl-ref th{background:#012851;color:#a1dcd8;font-weight:700;text-transform:uppercase;letter-spacing:.5px;font-size:9px;padding:7px 10px;text-align:left}
.impl-ref td{padding:7px 10px;border-bottom:1px solid #E0DDD6;vertical-align:top;line-height:1.6}
.impl-ref tr:hover td{background:#F0EFE9}
.impl-ref code{font-family:monospace;background:#F0EFE9;padding:1px 4px;border-radius:2px;font-size:10px;color:#012851}
.impl-ref .new{color:#3a6e42;font-weight:600}
.impl-ref .warn{color:#92400e;font-weight:600}
.impl-ref .sub-header td{background:#F0EFE9;font-weight:700;font-size:10px;color:#012851;padding:6px 10px}
</style>
</head>
<body>
<div class="page">
<!-- ══════════════════════════════════════════ MASTHEAD -->
<div class="mh">
<h1>Admin — OCR: Übersicht &amp; Modell-Detail</h1>
<p>Neue Admin-Sektion für das OCR-Pipeline-Monitoring. <strong>Zwei Seiten:</strong> Die Übersicht zeigt Systemstatus, globale Block-Statistiken und alle Modelle (global + sendergebunden) in einer Tabelle auf einen Blick. Die Modell-Detailseite zeigt die aktuellen Trainingsmetriken und die vollständige Trainingshistorie für ein einzelnes Modell, mit Aktions-Buttons direkt über der Tabelle. Tägliche Nutzung — Statistiken und aktive Läufe sind daher immer sofort sichtbar.</p>
<div class="tag-row">
<span class="tag">Route: /admin/ocr</span>
<span class="tag">Route: /admin/ocr/global</span>
<span class="tag">Route: /admin/ocr/[personId]</span>
<span class="tag amber">Neue EntityNav-Sektion: System → OCR</span>
<span class="tag green">Breakpoints: 320 / 640 / 1024 / 1440</span>
<span class="tag gray">API: GET /api/ocr/training-info · POST /api/ocr/train · POST /api/ocr/segtrain</span>
</div>
<p class="byline">Leonie Voss · UI/UX Design Lead · Familienarchiv · 2026-04-17</p>
</div>
<!-- ══════════════════════════════════════════ S1: OVERVIEW DESKTOP -->
<div class="sh">
<h2>1 — Übersicht: Desktop (≥1024px)</h2>
<p>Die Übersichtsseite öffnet sich beim Klick auf „OCR" in der EntityNav. Kein Split-Panel — OCR ist kein Entity-Editor. Der volle Bereich rechts der EntityNav gehört dem Dashboard. Vertikaler Fluss: Health-Bar → Statistiken → Modelltabelle → Globale Aktionen. Beim Klick auf eine Tabellenzeile wird die Modell-Detailseite geöffnet.</p>
</div>
<div class="grid">
<div class="col">
<div class="lbl">Desktop lg (1280px) — kein aktiver Lauf</div>
<div class="chrome" style="width:940px">
<div class="bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="url"></span></div>
<div class="app-header">
<span class="app-logo">FAMILIENARCHIV</span>
<span class="app-link">Dokumente</span>
<span class="app-link">Personen</span>
<span class="app-link act">Admin</span>
<div class="app-nav-r"><div class="app-av">M</div></div>
</div>
<div class="admin-shell" style="height:530px">
<!-- EntityNav -->
<div class="entity-nav">
<div class="en-heading">Admin</div>
<div class="en-item"><div class="en-icon"></div><div class="en-count">5</div><div class="en-label">Benutzer</div></div>
<div class="en-item"><div class="en-icon"></div><div class="en-count">3</div><div class="en-label">Gruppen</div></div>
<div class="en-item"><div class="en-icon"></div><div class="en-count">142</div><div class="en-label">Schlagwörter</div></div>
<div class="en-spacer"></div>
<div class="en-sep"></div>
<div class="en-heading" style="padding-top:5px">System</div>
<div class="en-item en-active"><div class="en-icon mint"></div><div class="en-count mint">7</div><div class="en-label mint">OCR</div></div>
</div>
<div class="v-line"></div>
<!-- OCR Content: idle state -->
<div class="ocr-content">
<div class="ocr-page-header">
<span class="oph-title">OCR-Administration</span>
</div>
<div class="ocr-scroll">
<!-- Health bar: idle -->
<div class="health-bar">
<span class="pill online">● Service online</span>
<div class="hb-sep"></div>
<span style="font-size:4.5px;color:#6b7280;">Kein aktiver Trainingsauftrag</span>
<div class="hb-spacer"></div>
<span style="font-size:4.5px;color:#9ca3af;">Letzter Lauf: sender_anna_raddatz · vor 2 Std.</span>
</div>
<!-- Stat row -->
<div class="stat-row">
<div class="stat-card">
<div class="sc-num">1.842</div>
<div class="sc-lbl">Eligible Blocks</div>
<div class="sc-sub">Kurrent-Erkennung</div>
</div>
<div class="stat-card">
<div class="sc-num">23</div>
<div class="sc-lbl">Dokumente</div>
<div class="sc-sub">mit annotierten Blöcken</div>
</div>
<div class="stat-card">
<div class="sc-num">310</div>
<div class="sc-lbl">Seg. Blocks</div>
<div class="sc-sub">Segmentierungstraining</div>
</div>
</div>
<!-- Models table -->
<div class="models-table">
<div class="mt-header-row">
<span class="mt-section-lbl">Alle Modelle</span>
</div>
<div class="mt-thead">
<span class="mt-th">Modell / Absender</span>
<span class="mt-th">Genauigkeit</span>
<span class="mt-th">ZFR ①</span>
<span class="mt-th">Blöcke</span>
<span class="mt-th">Letzter Lauf</span>
<span class="mt-th">Status</span>
</div>
<!-- global row -->
<div class="mt-row global-row">
<div><div class="mt-name">global_kurrent</div><div class="mt-person">Globales Erkennungsmodell</div></div>
<span class="mt-val">95,1 %</span>
<span class="mt-val">2,3 %</span>
<span class="mt-val">1.842</span>
<span class="mt-val" style="color:#6b7280;">vor 1 Tag</span>
<span><span class="pill done">✓ Fertig</span></span>
</div>
<!-- sender rows -->
<div class="mt-row">
<div><div class="mt-name sender">Anna Raddatz</div><div class="mt-person">sender_anna_raddatz</div></div>
<span class="mt-val good">98,3 %</span>
<span class="mt-val">1,7 %</span>
<span class="mt-val">128</span>
<span class="mt-val" style="color:#6b7280;">vor 2 Std.</span>
<span><span class="pill done">✓ Fertig</span></span>
</div>
<div class="mt-row">
<div><div class="mt-name sender">Franz Kaufmann</div><div class="mt-person">sender_franz_kaufmann</div></div>
<span class="mt-val bad"></span>
<span class="mt-val muted"></span>
<span class="mt-val">45</span>
<span class="mt-val" style="color:#6b7280;">vor 3 Tagen</span>
<span><span class="pill failed">✕ Fehler</span></span>
</div>
<div class="mt-row">
<div><div class="mt-name sender">Marie Schulz</div><div class="mt-person">sender_marie_schulz</div></div>
<span class="mt-val">96,7 %</span>
<span class="mt-val">1,9 %</span>
<span class="mt-val">203</span>
<span class="mt-val" style="color:#6b7280;">vor 3 Tagen</span>
<span><span class="pill done">✓ Fertig</span></span>
</div>
<div class="mt-row">
<div><div class="mt-name sender">Johann Raddatz</div><div class="mt-person">sender_johann_raddatz</div></div>
<span class="mt-val">94,2 %</span>
<span class="mt-val">2,9 %</span>
<span class="mt-val">87</span>
<span class="mt-val" style="color:#6b7280;">vor 1 Woche</span>
<span><span class="pill done">✓ Fertig</span></span>
</div>
<div class="mt-row" style="opacity:.5">
<div><div class="mt-name sender" style="color:#9ca3af;">+ 3 weitere Modelle</div></div>
<span class="mt-val muted"></span><span class="mt-val muted"></span><span class="mt-val muted"></span><span class="mt-val muted"></span><span></span>
</div>
</div>
<!-- Global actions -->
<div class="actions-section">
<div class="as-lbl">Globale Aktionen</div>
<div class="as-row">
<span class="btn-primary">Training Erkennung</span>
<span class="btn-primary">Training Segmentierung</span>
<span class="btn-outline">Erkennungsdaten exportieren</span>
<span class="btn-outline">Segmentierungsdaten exportieren</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Second state: active run -->
<div class="lbl" style="margin-top:18px">Desktop lg — mit aktivem Lauf (Health-Bar + Tabellenzeile)</div>
<div class="chrome" style="width:940px">
<div class="bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="url"></span></div>
<div class="app-header">
<span class="app-logo">FAMILIENARCHIV</span>
<span class="app-link">Dokumente</span>
<span class="app-link">Personen</span>
<span class="app-link act">Admin</span>
<div class="app-nav-r"><div class="app-av">M</div></div>
</div>
<div class="admin-shell" style="height:220px">
<div class="entity-nav">
<div class="en-heading">Admin</div>
<div class="en-item"><div class="en-icon"></div><div class="en-count">5</div><div class="en-label">Benutzer</div></div>
<div class="en-item"><div class="en-icon"></div><div class="en-count">3</div><div class="en-label">Gruppen</div></div>
<div class="en-item"><div class="en-icon"></div><div class="en-count">142</div><div class="en-label">Schlagwörter</div></div>
<div class="en-spacer"></div><div class="en-sep"></div>
<div class="en-heading" style="padding-top:5px">System</div>
<div class="en-item en-active"><div class="en-icon mint"></div><div class="en-count mint">7</div><div class="en-label mint">OCR</div></div>
</div>
<div class="v-line"></div>
<div class="ocr-content">
<div class="ocr-page-header"><span class="oph-title">OCR-Administration</span></div>
<div class="ocr-scroll">
<!-- Health bar: running + queued -->
<div class="health-bar">
<span class="pill online">● Service online</span>
<div class="hb-sep"></div>
<span class="pill running">⟳ sender_anna_raddatz · läuft seit 4 Min.</span>
<div class="hb-sep"></div>
<span style="font-size:4.5px;color:#92400e;background:#fef3c7;border:1px solid #fde68a;padding:1px 5px;border-radius:2px;font-weight:700;">⏳ 1 in Warteschlange</span>
<div class="hb-spacer"></div>
</div>
<!-- Table snippet showing running + queued rows -->
<div class="models-table">
<div class="mt-header-row"><span class="mt-section-lbl">Alle Modelle</span></div>
<div class="mt-thead">
<span class="mt-th">Modell / Absender</span><span class="mt-th">Genauigkeit</span><span class="mt-th">ZFR</span><span class="mt-th">Blöcke</span><span class="mt-th">Letzter Lauf</span><span class="mt-th">Status</span>
</div>
<div class="mt-row global-row">
<div><div class="mt-name">global_kurrent</div><div class="mt-person">Globales Erkennungsmodell</div></div>
<span class="mt-val">95,1 %</span><span class="mt-val">2,3 %</span><span class="mt-val">1.842</span>
<span class="mt-val" style="color:#6b7280;">vor 1 Tag</span><span><span class="pill done">✓ Fertig</span></span>
</div>
<div class="mt-row" style="background:#fff8e1;">
<div><div class="mt-name sender">Anna Raddatz</div><div class="mt-person">sender_anna_raddatz</div></div>
<span class="mt-val muted">98,3 %</span><span class="mt-val muted">1,7 %</span><span class="mt-val">128</span>
<span class="mt-val" style="color:#f57c00;font-weight:600;">gerade jetzt</span><span><span class="pill running">⟳ Läuft</span></span>
</div>
<div class="mt-row" style="opacity:.65;">
<div><div class="mt-name sender">Marie Schulz</div><div class="mt-person">sender_marie_schulz</div></div>
<span class="mt-val muted"></span><span class="mt-val muted"></span><span class="mt-val muted">203</span>
<span class="mt-val muted"></span><span><span class="pill queued">⏳ Warteschlange</span></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- /col -->
</div><!-- /grid -->
<div class="annotation">
<div class="ann-bullet">1</div>
<div class="ann-text"><strong>Globales Modell immer an Position 0.</strong> Die <code>global_kurrent</code>-Zeile hat einen leicht bläulichen Hintergrund (<code>bg-[#F8F8FC]</code>) und ist unabhängig von Sortierung immer die erste Zeile. Sie wird anhand <code>personId === null</code> identifiziert.</div>
</div>
<div class="annotation">
<div class="ann-bullet">2</div>
<div class="ann-text"><strong>Health-Bar als Echtzeit-Primärindikator.</strong> Der Status (RUNNING, QUEUED, idle) wird via SSE aktualisiert — derselbe <code>/api/ocr/jobs/{jobId}/progress</code>-Kanal wie in der Dokumentenansicht. RUNNING-Tabellenzeilen erhalten zusätzlich <code>bg-amber-50</code>. Die Buttons „Training Erkennung" und „Training Segmentierung" werden <code>disabled + opacity-50</code> wenn ein Lauf aktiv ist.</div>
</div>
<div class="annotation">
<div class="ann-bullet">3</div>
<div class="ann-text"><strong>① ZFR = Zeichenfehlerrate (CER).</strong> Abkürzung im <code>&lt;th&gt;</code> mit <code>title="Zeichenfehlerrate (Character Error Rate)"</code> und <code>aria-label</code> für Screenreader. Kleinere Werte sind besser — ein Pfeil in der Implementierung ist optional.</div>
</div>
<!-- ══════════════════════════════════════════ S2: OVERVIEW MOBILE -->
<div class="sh">
<h2>2 — Übersicht: Mobile (320px)</h2>
<p>Auf kleinen Bildschirmen stapeln sich die drei Stat-Karten einspaltig. Die Modelltabelle zeigt nur Name und Status-Pill (keine Metriken-Spalten — zu schmal). Aktions-Buttons werden vollbreit und vertikal gestapelt.</p>
</div>
<div class="grid">
<div class="col">
<div class="lbl">320px</div>
<div class="phone">
<div class="phone-status"><span>09:41</span><span>●●●●</span></div>
<div class="phone-body">
<!-- App header mobile -->
<div class="app-header" style="height:22px;padding:0 8px;gap:6px;">
<span class="app-logo" style="font-size:5.5px;">FAMILIENARCHIV</span>
<div style="margin-left:auto;display:flex;flex-direction:column;justify-content:center;gap:2px;width:12px;">
<div style="height:1.5px;background:rgba(255,255,255,.5);border-radius:1px;"></div>
<div style="height:1.5px;background:rgba(255,255,255,.5);border-radius:1px;"></div>
<div style="height:1.5px;background:rgba(255,255,255,.5);border-radius:1px;"></div>
</div>
</div>
<!-- Page header -->
<div style="background:white;border-bottom:1px solid #e4e2d7;padding:4px 7px;">
<span style="font-size:6.5px;font-weight:800;color:#012851;">OCR-Administration</span>
</div>
<!-- Health bar compact -->
<div style="display:flex;align-items:center;gap:4px;padding:3px 7px;background:white;border-bottom:1px solid #e4e2d7;">
<span class="pill online" style="font-size:4px;padding:1px 4px;">● Online</span>
<span style="font-size:4px;color:#6b7280;">Kein aktiver Lauf</span>
</div>
<div class="phone-scroll">
<!-- Stat cards stacked -->
<div class="stat-card" style="padding:6px 8px;">
<div class="sc-num" style="font-size:16px;">1.842</div>
<div class="sc-lbl">Eligible Blocks</div>
</div>
<div class="stat-card" style="padding:6px 8px;">
<div class="sc-num" style="font-size:16px;">23</div>
<div class="sc-lbl">Dokumente</div>
</div>
<div class="stat-card" style="padding:6px 8px;">
<div class="sc-num" style="font-size:16px;">310</div>
<div class="sc-lbl">Seg. Blocks</div>
</div>
<!-- Models list simplified -->
<div class="models-table">
<div class="mt-header-row"><span class="mt-section-lbl">Alle Modelle</span></div>
<div style="display:grid;grid-template-columns:1fr auto;gap:4px;padding:4px 7px;border-bottom:1px solid #f0efe9;background:#F8F8FC;align-items:center;">
<div><div style="font-size:6px;font-weight:700;color:#012851;">global_kurrent</div><div style="font-size:4px;color:#9ca3af;">95,1 % · 1.842 Bl.</div></div>
<span class="pill done" style="font-size:4px;padding:1px 4px;"></span>
</div>
<div style="display:grid;grid-template-columns:1fr auto;gap:4px;padding:4px 7px;border-bottom:1px solid #f0efe9;align-items:center;">
<div><div style="font-size:6px;color:#374151;">Anna Raddatz</div><div style="font-size:4px;color:#9ca3af;">98,3 % · 128 Bl.</div></div>
<span class="pill done" style="font-size:4px;padding:1px 4px;"></span>
</div>
<div style="display:grid;grid-template-columns:1fr auto;gap:4px;padding:4px 7px;border-bottom:1px solid #f0efe9;align-items:center;">
<div><div style="font-size:6px;color:#374151;">Franz Kaufmann</div><div style="font-size:4px;color:#c62828;">Fehler · 45 Bl.</div></div>
<span class="pill failed" style="font-size:4px;padding:1px 4px;"></span>
</div>
<div style="display:grid;grid-template-columns:1fr auto;gap:4px;padding:4px 7px;border-bottom:1px solid #f0efe9;align-items:center;">
<div><div style="font-size:6px;color:#374151;">Marie Schulz</div><div style="font-size:4px;color:#9ca3af;">96,7 % · 203 Bl.</div></div>
<span class="pill done" style="font-size:4px;padding:1px 4px;"></span>
</div>
<div style="padding:4px 7px;font-size:4.5px;color:#9ca3af;text-align:center;">+ 4 weitere…</div>
</div>
<!-- Action buttons full-width -->
<span class="btn-primary" style="justify-content:center;height:22px;font-size:5.5px;">Training Erkennung</span>
<span class="btn-primary" style="justify-content:center;height:22px;font-size:5.5px;">Training Segmentierung</span>
<span class="btn-outline" style="justify-content:center;height:22px;font-size:5.5px;">Erkennungsdaten exportieren</span>
</div>
</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════ S3: DETAIL DESKTOP -->
<div class="sh">
<h2>3 — Modell-Detail: Desktop (≥1024px)</h2>
<p>Die Detailseite öffnet sich beim Klick auf eine Zeile der Übersichtstabelle. Sie folgt demselben vertikalen Muster: Metric-Cards oben, darunter die vollständige Trainingshistorie nur für dieses Modell. Für Sender-Modelle gibt es einen Link zur Personen-Detailseite. Zwei Varianten: Sender-Modell (mit Personen-Link) und globales Modell (ohne).</p>
</div>
<div class="grid">
<div class="col">
<div class="lbl">Desktop lg — sender_anna_raddatz (Lauf aktiv)</div>
<div class="chrome" style="width:940px">
<div class="bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="url"></span></div>
<div class="app-header">
<span class="app-logo">FAMILIENARCHIV</span>
<span class="app-link">Dokumente</span>
<span class="app-link">Personen</span>
<span class="app-link act">Admin</span>
<div class="app-nav-r"><div class="app-av">M</div></div>
</div>
<div class="admin-shell" style="height:490px">
<div class="entity-nav">
<div class="en-heading">Admin</div>
<div class="en-item"><div class="en-icon"></div><div class="en-count">5</div><div class="en-label">Benutzer</div></div>
<div class="en-item"><div class="en-icon"></div><div class="en-count">3</div><div class="en-label">Gruppen</div></div>
<div class="en-item"><div class="en-icon"></div><div class="en-count">142</div><div class="en-label">Schlagwörter</div></div>
<div class="en-spacer"></div><div class="en-sep"></div>
<div class="en-heading" style="padding-top:5px">System</div>
<div class="en-item en-active"><div class="en-icon mint"></div><div class="en-count mint">7</div><div class="en-label mint">OCR</div></div>
</div>
<div class="v-line"></div>
<div class="ocr-content">
<!-- Page header: sender model with active run -->
<div class="ocr-page-header">
<span class="oph-back">← OCR</span>
<span class="oph-title">sender_anna_raddatz</span>
<span class="oph-sub">· Anna Raddatz</span>
<span class="pill running" style="margin-left:auto;">⟳ Läuft</span>
</div>
<div class="ocr-scroll">
<!-- Metric cards -->
<div class="metric-row">
<div class="metric-card">
<div class="mc-num good">98,3 %</div>
<div class="mc-lbl">Genauigkeit</div>
<div class="mc-delta up">↑ von 97,1 % (vorher)</div>
</div>
<div class="metric-card">
<div class="mc-num neutral">1,7 %</div>
<div class="mc-lbl">Zeichenfehlerrate</div>
<div class="mc-delta up">↓ von 2,2 % (vorher)</div>
</div>
<div class="metric-card">
<div class="mc-num neutral">128</div>
<div class="mc-lbl">Training-Blöcke</div>
<div class="mc-delta dim">beim letzten Lauf</div>
</div>
<div class="metric-card">
<div class="mc-num neutral">50</div>
<div class="mc-lbl">Epochs</div>
<div class="mc-delta dim">letztes Training</div>
</div>
</div>
<!-- Section bar above history table -->
<div class="section-bar">
<span class="section-bar-lbl">Trainingshistorie</span>
<span class="btn-primary">Jetzt neu trainieren</span>
<span class="btn-outline">Training-Daten exportieren</span>
<a class="btn-link">→ Anna Raddatz</a>
</div>
<!-- History table -->
<div class="hist-table">
<div class="ht-thead">
<span class="ht-th">Status</span>
<span class="ht-th">Zeitpunkt</span>
<span class="ht-th">Genauigkeit</span>
<span class="ht-th">ZFR</span>
<span class="ht-th">Blöcke</span>
<span class="ht-th">Epochs</span>
</div>
<div class="ht-row running">
<span><span class="pill running">⟳ Läuft</span></span>
<span class="ht-val" style="color:#6b7280;">gerade jetzt</span>
<span class="ht-val muted"></span>
<span class="ht-val muted"></span>
<span class="ht-val">128</span>
<span class="ht-val muted"></span>
</div>
<div class="ht-row">
<span><span class="pill done">✓ Fertig</span></span>
<span class="ht-val" style="color:#6b7280;">vor 2 Std.</span>
<span class="ht-val good">98,3 %</span>
<span class="ht-val">1,7 %</span>
<span class="ht-val">128</span>
<span class="ht-val">50</span>
</div>
<div class="ht-row">
<span><span class="pill done">✓ Fertig</span></span>
<span class="ht-val" style="color:#6b7280;">vor 3 Tagen</span>
<span class="ht-val">97,1 %</span>
<span class="ht-val">2,2 %</span>
<span class="ht-val">92</span>
<span class="ht-val">50</span>
</div>
<div class="ht-row">
<span><span class="pill done">✓ Fertig</span></span>
<span class="ht-val" style="color:#6b7280;">vor 1 Woche</span>
<span class="ht-val">96,4 %</span>
<span class="ht-val">2,8 %</span>
<span class="ht-val">74</span>
<span class="ht-val">40</span>
</div>
<div class="ht-row failed-row">
<span><span class="pill failed">✕ Fehler</span></span>
<span class="ht-val" style="color:#6b7280;">vor 3 Wochen</span>
<span class="ht-val muted"></span>
<span class="ht-val muted"></span>
<span class="ht-val">60</span>
<span class="ht-val muted"></span>
</div>
<!-- Error detail inline row -->
<div style="padding:2px 9px 5px;background:#fff8f8;border-bottom:1px solid #f0efe9;">
<span class="ht-err">Fehler: Connection timeout nach 120 s — OCR-Service nicht erreichbar</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Global model variant -->
<div class="lbl" style="margin-top:18px">Desktop lg — global_kurrent (kein Personen-Link)</div>
<div class="chrome" style="width:940px">
<div class="bar"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="url"></span></div>
<div class="app-header">
<span class="app-logo">FAMILIENARCHIV</span>
<span class="app-link">Dokumente</span>
<span class="app-link">Personen</span>
<span class="app-link act">Admin</span>
<div class="app-nav-r"><div class="app-av">M</div></div>
</div>
<div class="admin-shell" style="height:320px">
<div class="entity-nav">
<div class="en-heading">Admin</div>
<div class="en-item"><div class="en-icon"></div><div class="en-count">5</div><div class="en-label">Benutzer</div></div>
<div class="en-item"><div class="en-icon"></div><div class="en-count">3</div><div class="en-label">Gruppen</div></div>
<div class="en-item"><div class="en-icon"></div><div class="en-count">142</div><div class="en-label">Schlagwörter</div></div>
<div class="en-spacer"></div><div class="en-sep"></div>
<div class="en-heading" style="padding-top:5px">System</div>
<div class="en-item en-active"><div class="en-icon mint"></div><div class="en-count mint">7</div><div class="en-label mint">OCR</div></div>
</div>
<div class="v-line"></div>
<div class="ocr-content">
<div class="ocr-page-header">
<span class="oph-back">← OCR</span>
<span class="oph-title">global_kurrent</span>
<span class="oph-sub">· Globales Erkennungsmodell</span>
<span class="pill done" style="margin-left:auto;">✓ Fertig</span>
</div>
<div class="ocr-scroll">
<div class="metric-row">
<div class="metric-card">
<div class="mc-num neutral">95,1 %</div>
<div class="mc-lbl">Genauigkeit</div>
<div class="mc-delta up">↑ von 94,8 % (vorher)</div>
</div>
<div class="metric-card">
<div class="mc-num neutral">2,3 %</div>
<div class="mc-lbl">Zeichenfehlerrate</div>
<div class="mc-delta up">↓ von 2,7 % (vorher)</div>
</div>
<div class="metric-card">
<div class="mc-num neutral">1.842</div>
<div class="mc-lbl">Training-Blöcke</div>
<div class="mc-delta dim">beim letzten Lauf</div>
</div>
<div class="metric-card">
<div class="mc-num neutral">100</div>
<div class="mc-lbl">Epochs</div>
<div class="mc-delta dim">letztes Training</div>
</div>
</div>
<div class="section-bar">
<span class="section-bar-lbl">Trainingshistorie</span>
<span class="btn-primary">Jetzt neu trainieren</span>
<span class="btn-outline">Training-Daten exportieren</span>
<!-- kein Personen-Link für globales Modell -->
</div>
<div class="hist-table">
<div class="ht-thead">
<span class="ht-th">Status</span><span class="ht-th">Zeitpunkt</span><span class="ht-th">Genauigkeit</span><span class="ht-th">ZFR</span><span class="ht-th">Blöcke</span><span class="ht-th">Epochs</span>
</div>
<div class="ht-row">
<span><span class="pill done">✓ Fertig</span></span>
<span class="ht-val" style="color:#6b7280;">vor 1 Tag</span>
<span class="ht-val good">95,1 %</span><span class="ht-val">2,3 %</span><span class="ht-val">1.842</span><span class="ht-val">100</span>
</div>
<div class="ht-row">
<span><span class="pill done">✓ Fertig</span></span>
<span class="ht-val" style="color:#6b7280;">vor 5 Tagen</span>
<span class="ht-val">94,8 %</span><span class="ht-val">2,7 %</span><span class="ht-val">1.650</span><span class="ht-val">100</span>
</div>
<div class="ht-row">
<span><span class="pill done">✓ Fertig</span></span>
<span class="ht-val" style="color:#6b7280;">vor 2 Wochen</span>
<span class="ht-val">93,2 %</span><span class="ht-val">3,1 %</span><span class="ht-val">1.420</span><span class="ht-val">100</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- /col -->
</div><!-- /grid -->
<div class="annotation">
<div class="ann-bullet">4</div>
<div class="ann-text"><strong>Delta-Zeile in Metric-Cards.</strong> Unter jedem Metrikwert erscheint ein Vergleich zum vorherigen erfolgreichen Lauf (<code>↑ / ↓ von X % (vorher)</code>). Grün für Verbesserung, rot für Verschlechterung. Berechnung: letzter DONE-Lauf vs. vorletzter DONE-Lauf im nach <code>completedAt</code> sortierten Array. Entfällt wenn weniger als zwei DONE-Läufe vorhanden.</div>
</div>
<div class="annotation">
<div class="ann-bullet">5</div>
<div class="ann-text"><strong>FAILED-Fehlertext inline.</strong> Bei einer FAILED-Zeile erscheint direkt darunter eine zweite Zeile mit dem <code>errorMessage</code>-Feld — leicht rosa hinterlegt, kein Modal nötig. Ist <code>errorMessage</code> null oder leer, entfällt die zweite Zeile.</div>
</div>
<div class="annotation">
<div class="ann-bullet">6</div>
<div class="ann-text"><strong>Keine Danger-Zone.</strong> Sender-Modelle werden programmatisch durch <code>SenderModelService</code> angelegt und gelöscht. Die Detailseite bietet bewusst keinen manuellen Lösch-Button. Beim globalen Modell fehlt der „→ Person"-Link, da <code>personId === null</code>.</div>
</div>
<!-- ══════════════════════════════════════════ S4: DETAIL MOBILE -->
<div class="sh">
<h2>4 — Modell-Detail: Mobile (320px)</h2>
<p>Die vier Metric-Cards arrangieren sich auf Mobile in einem 2×2-Raster. Aktions-Buttons nehmen die volle Breite ein. Die Trainingshistorie zeigt auf kleinen Bildschirmen nur Status, Datum und Genauigkeit — ZFR, Blöcke und Epochs entfallen.</p>
</div>
<div class="grid">
<div class="col">
<div class="lbl">320px — sender_anna_raddatz</div>
<div class="phone">
<div class="phone-status"><span>09:41</span><span>●●●●</span></div>
<div class="phone-body">
<div class="app-header" style="height:22px;padding:0 8px;gap:6px;">
<span class="app-logo" style="font-size:5.5px;">FAMILIENARCHIV</span>
<div style="margin-left:auto;display:flex;flex-direction:column;justify-content:center;gap:2px;width:12px;">
<div style="height:1.5px;background:rgba(255,255,255,.5);border-radius:1px;"></div>
<div style="height:1.5px;background:rgba(255,255,255,.5);border-radius:1px;"></div>
<div style="height:1.5px;background:rgba(255,255,255,.5);border-radius:1px;"></div>
</div>
</div>
<!-- Page header mobile -->
<div style="background:white;border-bottom:1px solid #e4e2d7;padding:4px 7px;display:flex;align-items:center;gap:4px;">
<span style="font-size:5px;color:#9ca3af;flex-shrink:0;">← OCR</span>
<div style="flex:1;min-width:0;">
<div style="font-size:6px;font-weight:800;color:#012851;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">sender_anna_raddatz</div>
<div style="font-size:4px;color:#6b7280;">Anna Raddatz</div>
</div>
<span class="pill running" style="font-size:4px;padding:1px 4px;flex-shrink:0;"></span>
</div>
<div class="phone-scroll">
<!-- Metric cards 2x2 -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;">
<div class="metric-card" style="padding:6px 8px;">
<div class="mc-num good" style="font-size:14px;">98,3 %</div>
<div class="mc-lbl" style="font-size:4px;">Genauigkeit</div>
<div class="mc-delta up" style="font-size:4px;">↑ von 97,1 %</div>
</div>
<div class="metric-card" style="padding:6px 8px;">
<div class="mc-num neutral" style="font-size:14px;">1,7 %</div>
<div class="mc-lbl" style="font-size:4px;">ZFR</div>
<div class="mc-delta up" style="font-size:4px;">↓ von 2,2 %</div>
</div>
<div class="metric-card" style="padding:6px 8px;">
<div class="mc-num neutral" style="font-size:14px;">128</div>
<div class="mc-lbl" style="font-size:4px;">Blöcke</div>
<div class="mc-delta dim" style="font-size:4px;">letzter Lauf</div>
</div>
<div class="metric-card" style="padding:6px 8px;">
<div class="mc-num neutral" style="font-size:14px;">50</div>
<div class="mc-lbl" style="font-size:4px;">Epochs</div>
<div class="mc-delta dim" style="font-size:4px;">letzter Lauf</div>
</div>
</div>
<!-- Action buttons full-width -->
<span class="btn-primary" style="justify-content:center;height:22px;font-size:5.5px;">Jetzt neu trainieren</span>
<span class="btn-outline" style="justify-content:center;height:22px;font-size:5.5px;">Training-Daten exportieren</span>
<a style="font-size:5px;color:#012851;text-decoration:underline;text-align:center;padding:2px 0;">→ Anna Raddatz</a>
<!-- Training history simplified -->
<div class="hist-table">
<div style="padding:4px 7px;border-bottom:1px solid #e4e2d7;background:#f5f4ef;">
<span style="font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#6b7280;">Trainingshistorie</span>
</div>
<div style="background:#fff8e1;padding:4px 7px;border-bottom:1px solid #f0efe9;display:flex;align-items:center;gap:4px;">
<span class="pill running" style="font-size:4px;padding:1px 4px;"></span>
<span style="font-size:5px;color:#6b7280;flex:1;">gerade jetzt · 128 Bl.</span>
</div>
<div style="padding:4px 7px;border-bottom:1px solid #f0efe9;display:flex;align-items:center;gap:4px;">
<span class="pill done" style="font-size:4px;padding:1px 4px;"></span>
<span style="font-size:5px;color:#6b7280;flex:1;">vor 2 Std.</span>
<span style="font-size:5.5px;color:#2e7d32;font-weight:700;">98,3 %</span>
</div>
<div style="padding:4px 7px;border-bottom:1px solid #f0efe9;display:flex;align-items:center;gap:4px;">
<span class="pill done" style="font-size:4px;padding:1px 4px;"></span>
<span style="font-size:5px;color:#6b7280;flex:1;">vor 3 Tagen</span>
<span style="font-size:5.5px;color:#374151;">97,1 %</span>
</div>
<div style="padding:4px 7px;border-bottom:1px solid #f0efe9;display:flex;align-items:center;gap:4px;">
<span class="pill done" style="font-size:4px;padding:1px 4px;"></span>
<span style="font-size:5px;color:#6b7280;flex:1;">vor 1 Woche</span>
<span style="font-size:5.5px;color:#374151;">96,4 %</span>
</div>
<div style="padding:4px 7px;background:#fff8f8;display:flex;align-items:center;gap:4px;">
<span class="pill failed" style="font-size:4px;padding:1px 4px;"></span>
<span style="font-size:5px;color:#c62828;flex:1;">vor 3 Wochen · Timeout</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════ S5: IMPLEMENTATION REFERENCE -->
<div class="sh">
<h2>5 — Implementation Reference</h2>
<p>Exakte Tailwind-Klassen und Pixelwerte für beide Seiten. Neue Dateien sind mit <span class="impl-ref" style="display:inline"><span class="new">Neu</span></span> markiert.</p>
</div>
<div class="impl-ref">
<table>
<thead>
<tr><th>Element</th><th>Tailwind-Klassen</th><th>Real px / Wert</th><th>Hinweis</th></tr>
</thead>
<tbody>
<tr class="sub-header"><td colspan="4">Routen &amp; Dateien</td></tr>
<tr>
<td>Übersicht</td>
<td><code>src/routes/admin/ocr/+page.svelte</code><br><code>src/routes/admin/ocr/+page.server.ts</code></td>
<td><code>GET /api/ocr/training-info</code></td>
<td class="new">Neu</td>
</tr>
<tr>
<td>Global-Detail</td>
<td><code>src/routes/admin/ocr/global/+page.svelte</code></td>
<td><code>GET /api/ocr/training-info</code> (gefiltert: <code>personId === null</code>)</td>
<td class="new">Neu</td>
</tr>
<tr>
<td>Sender-Detail</td>
<td><code>src/routes/admin/ocr/[personId]/+page.svelte</code></td>
<td><code>GET /api/ocr/training-info</code> (gefiltert nach personId)</td>
<td class="new">Neu</td>
</tr>
<tr>
<td>EntityNav-Eintrag</td>
<td>Vorhandene <code>EntityNav</code>-Komponente um OCR-Eintrag ergänzen</td>
<td>Neue Sektion „System" mit Separator davor</td>
<td class="warn">Bestehende Datei ändern</td>
</tr>
<tr class="sub-header"><td colspan="4">Seitenstruktur (beide Seiten)</td></tr>
<tr>
<td>OCR Content Area</td>
<td><code>flex-1 flex flex-col overflow-hidden min-w-0 bg-surface</code></td>
<td>Kein Tree-Panel, kein Edit-Panel — volle Breite nach EntityNav</td>
<td>OCR ist kein Entity-Editor; enthält kein Split-Layout</td>
</tr>
<tr>
<td>Page Header Bar</td>
<td><code>flex items-center gap-2 px-4 py-2.5 border-b border-line bg-white shrink-0 h-10</code></td>
<td>h: 40px</td>
<td>Enthält Back-Link (nur Detail), Titel, optionale Status-Pill</td>
</tr>
<tr>
<td>Scrollbarer Seiteninhalt</td>
<td><code>flex-1 overflow-y-auto p-4 flex flex-col gap-3</code></td>
<td>gap: 12px zwischen Sektionen</td>
<td></td>
</tr>
<tr>
<td>Back-Link (Detail)</td>
<td><code>text-[11px] text-gray-400 hover:text-ink transition-colors shrink-0</code></td>
<td></td>
<td><code>goto('/admin/ocr')</code> oder <code>&lt;a href="/admin/ocr"&gt;</code></td>
</tr>
<tr class="sub-header"><td colspan="4">Health Bar (Übersicht)</td></tr>
<tr>
<td>Health Bar Container</td>
<td><code>flex items-center gap-3 px-4 py-2 bg-white border border-line rounded-sm</code></td>
<td>h: 36px</td>
<td class="new">Neu</td>
</tr>
<tr>
<td>Service-Pill (online)</td>
<td><code>inline-flex items-center gap-1 px-2 py-0.5 rounded-sm text-[10px] font-bold uppercase tracking-wide bg-green-50 text-green-700 border border-green-200</code></td>
<td></td>
<td>Offline: <code>bg-red-50 text-red-700 border-red-200</code></td>
</tr>
<tr>
<td>Running-Pill</td>
<td><code>bg-amber-50 text-amber-700 border border-amber-200</code> (gleiche Basis)</td>
<td></td>
<td>Nur sichtbar wenn <code>status === 'RUNNING'</code>. Icon ⟳ via <code>animate-spin</code></td>
</tr>
<tr>
<td>Queued-Badge</td>
<td><code>text-[10px] text-amber-800 bg-amber-100 border border-amber-200 px-2 py-0.5 rounded-sm font-bold</code></td>
<td></td>
<td>Nur sichtbar wenn mind. 1 Lauf QUEUED. Text: „⏳ N in Warteschlange"</td>
</tr>
<tr class="sub-header"><td colspan="4">Stat Cards (Übersicht)</td></tr>
<tr>
<td>Grid (Desktop)</td>
<td><code>grid grid-cols-1 sm:grid-cols-3 gap-3</code></td>
<td>Mobile: 1 Spalte; ab 640px: 3 Spalten</td>
<td class="new">Neu</td>
</tr>
<tr>
<td>Stat Card</td>
<td><code>bg-white border border-line rounded-sm px-4 py-3</code></td>
<td>padding: 12px 16px · kein Hover (nicht klickbar)</td>
<td></td>
</tr>
<tr>
<td>Stat-Zahl</td>
<td><code>font-serif text-3xl font-bold text-ink leading-none</code></td>
<td>30px, Merriweather · deutsche Tausend-Trennung via <code>n.toLocaleString('de-DE')</code></td>
<td></td>
</tr>
<tr>
<td>Stat-Label</td>
<td><code>text-[10px] font-bold uppercase tracking-widest text-gray-400 mt-1</code></td>
<td>10px</td>
<td></td>
</tr>
<tr>
<td>Stat-Subtext</td>
<td><code>text-[9px] text-gray-300 mt-0.5</code></td>
<td>9px · optional</td>
<td></td>
</tr>
<tr class="sub-header"><td colspan="4">Modell-Tabelle (Übersicht)</td></tr>
<tr>
<td>Tabellen-Container</td>
<td><code>bg-white border border-line rounded-sm overflow-hidden</code></td>
<td></td>
<td class="new">Neu — kein <code>&lt;table&gt;</code>, sondern CSS-Grid-Rows</td>
</tr>
<tr>
<td>Tabellenkopf</td>
<td><code>bg-ink grid px-4 py-2 grid-cols-[2fr_80px_80px_80px_80px_100px]</code></td>
<td></td>
<td>Kopfzellen: <code>text-[9px] font-bold uppercase tracking-wide text-brand-mint</code></td>
</tr>
<tr>
<td>Tabellenzeile</td>
<td><code>grid px-4 py-2.5 border-b border-line items-center cursor-pointer hover:bg-surface transition-colors grid-cols-[2fr_80px_80px_80px_80px_100px]</code></td>
<td>Klick → <code>goto('/admin/ocr/{id}')</code></td>
<td>Mobile: Tabelle horizontal scrollbar (<code>overflow-x-auto</code>) oder vereinfachte 2-Spalten-Liste</td>
</tr>
<tr>
<td>Global-Zeile</td>
<td>Zusätzlich <code>bg-[#F8F8FC]</code></td>
<td>Immer erste Zeile (<code>personId === null</code>)</td>
<td></td>
</tr>
<tr>
<td>Sender-Name</td>
<td><code>text-sm font-medium text-gray-700</code></td>
<td>14px · darunter modelName: <code>text-[10px] text-gray-400 mt-0.5</code></td>
<td>Name aus <code>personNames[run.personId]</code> des training-info-Endpoints</td>
</tr>
<tr>
<td>Metrik-Wert (gut)</td>
<td><code>text-green-700 font-semibold</code></td>
<td>Accuracy ≥ 95 % oder niedrigste CER</td>
<td></td>
</tr>
<tr>
<td>Metrik-Wert (Fehler)</td>
<td><code>text-red-600 font-semibold</code></td>
<td></td>
<td>Zeigt „—" bei fehlenden Werten (FAILED/RUNNING)</td>
</tr>
<tr class="sub-header"><td colspan="4">Status Pills (alle Seiten)</td></tr>
<tr>
<td>Basis-Klassen</td>
<td><code>inline-flex items-center gap-1 px-2 py-0.5 rounded-sm text-[10px] font-bold uppercase tracking-wide border</code></td>
<td></td>
<td>Icon als Textzeichen vor Label (✓ ⟳ ✕ ⏳)</td>
</tr>
<tr>
<td>DONE</td>
<td><code>bg-green-50 text-green-700 border-green-200</code></td>
<td></td><td></td>
</tr>
<tr>
<td>RUNNING</td>
<td><code>bg-amber-50 text-amber-700 border-amber-200</code></td>
<td></td>
<td>Icon ⟳ in eigenem <code>&lt;span class="animate-spin"&gt;</code></td>
</tr>
<tr>
<td>FAILED</td>
<td><code>bg-red-50 text-red-700 border-red-200</code></td>
<td></td><td></td>
</tr>
<tr>
<td>QUEUED</td>
<td><code>bg-yellow-50 text-yellow-800 border-yellow-200</code></td>
<td></td><td></td>
</tr>
<tr class="sub-header"><td colspan="4">Metric Cards (Detail)</td></tr>
<tr>
<td>Grid</td>
<td><code>grid grid-cols-2 sm:grid-cols-4 gap-3</code></td>
<td>Mobile: 2×2 · Desktop: 4×1</td>
<td class="new">Neu</td>
</tr>
<tr>
<td>Metric Card</td>
<td><code>bg-white border border-line rounded-sm px-4 py-3 text-center</code></td>
<td></td><td></td>
</tr>
<tr>
<td>Zahl (positiv)</td>
<td><code>font-serif text-3xl font-bold leading-none text-green-700</code></td>
<td>Accuracy ≥ 95 %</td><td></td>
</tr>
<tr>
<td>Zahl (neutral)</td>
<td><code>font-serif text-3xl font-bold leading-none text-ink</code></td>
<td>Standard</td><td></td>
</tr>
<tr>
<td>Delta-Zeile</td>
<td><code>text-[10px] mt-1</code> + <code>text-green-600</code> / <code>text-red-600</code> / <code>text-gray-400</code></td>
<td>10px</td>
<td>Format: „↑ von 94,8 % (vorher)". Entfällt bei &lt; 2 DONE-Läufen.</td>
</tr>
<tr class="sub-header"><td colspan="4">Trainingshistorie (Detail)</td></tr>
<tr>
<td>Tabellen-Container</td>
<td><code>bg-white border border-line rounded-sm overflow-hidden</code></td>
<td></td><td></td>
</tr>
<tr>
<td>Tabellenkopf</td>
<td><code>bg-ink grid px-4 py-2 grid-cols-[100px_1fr_80px_80px_80px_80px]</code></td>
<td></td>
<td>Mobile: <code>grid-cols-[80px_1fr_80px]</code> (Status · Datum · Genauigkeit)</td>
</tr>
<tr>
<td>RUNNING-Zeile</td>
<td><code>bg-amber-50</code> zusätzlich</td>
<td></td>
<td>Leere Metrik-Zellen zeigen „—"</td>
</tr>
<tr>
<td>FAILED-Zeile</td>
<td><code>bg-red-50</code> (sehr hell, ~#fff8f8)</td>
<td></td>
<td>Fehlertext folgt als zweite Sub-Zeile: <code>text-[11px] text-red-600 px-4 pb-2 col-span-full</code></td>
</tr>
<tr class="sub-header"><td colspan="4">Buttons &amp; Aktionen</td></tr>
<tr>
<td>Primär-Button</td>
<td><code>inline-flex items-center h-9 px-4 bg-ink text-white rounded-sm text-xs font-bold uppercase tracking-wide hover:opacity-90 transition-opacity</code></td>
<td>h: 36px (44px auf Mobile)</td>
<td>Disabled wenn Lauf aktiv: <code>opacity-50 cursor-not-allowed pointer-events-none</code></td>
</tr>
<tr>
<td>Outline-Button</td>
<td><code>inline-flex items-center h-9 px-4 border border-ink text-ink rounded-sm text-xs font-bold uppercase tracking-wide hover:bg-surface transition-colors</code></td>
<td></td>
<td>Export: <code>window.location.href = '/api/ocr/training-data/export'</code></td>
</tr>
<tr>
<td>Personen-Link</td>
<td><code>text-sm text-ink underline underline-offset-2 hover:text-brand-mint transition-colors</code></td>
<td></td>
<td>Nur auf Sender-Detailseite. Ziel: <code>/persons/{personId}</code>. Entfällt bei <code>personId === null</code>.</td>
</tr>
<tr class="sub-header"><td colspan="4">API-Aufrufe</td></tr>
<tr>
<td>Übersicht laden</td>
<td><code>GET /api/ocr/training-info</code></td>
<td>Felder: <code>availableBlocks · totalOcrBlocks · availableDocuments · availableSegBlocks · ocrServiceAvailable · runs · personNames</code></td>
<td>Im <code>+page.server.ts</code> laden; <code>SenderModel</code>-Liste zusätzlich via eigenem Endpunkt oder aus <code>runs</code> aggregieren</td>
</tr>
<tr>
<td>Training starten</td>
<td><code>POST /api/ocr/train</code> / <code>POST /api/ocr/segtrain</code></td>
<td>Response: <code>OcrTrainingRun</code></td>
<td>Sofort in Health-Bar und Tabelle anzeigen; SSE-Kanal öffnen</td>
</tr>
<tr>
<td>Live-Updates</td>
<td><code>EventSource('/api/ocr/jobs/{jobId}/progress')</code></td>
<td>Server-Sent Events</td>
<td>Health-Bar + Status-Pills live aktualisieren; <code>onDestroy</code> schließen</td>
</tr>
</tbody>
</table>
</div>
</div><!-- /page -->
</body>
</html>