root f408a555f8 feat: kontinuierliche LXC↔Mac-Diagnose + Auto-Retry
Diagnose-Sichtbarkeit (Plan A):
- Job.step_started_at + last_heartbeat_at, Heartbeat-Task pingt alle 3 s
  während laufender Mac-Calls
- Mac-Worker hält per X-Job-Id Header einen Log-Ringbuffer pro Job
  (200 Zeilen, 1 h TTL); GET /api/jobs/{id}/log liefert ihn aus
- LXC-Endpoint GET /api/jobs/{id}/diag bündelt Job-Status, Stage-Sekunden,
  Heartbeat-Alter und Mac-Worker-Log
- Frontend: Live-Timing im Status-Pill ("läuft seit Xs · letzter Ping vor Ys"),
  klappbares Diagnose-Panel mit Mac-Log für FAILED + langlaufende Jobs

Robustheit (Plan B):
- MacClient: einmaliger Retry mit 3 s Backoff bei RemoteProtocolError /
  ConnectError / ReadError für summarize/protocol/export
- Mac-Worker /api/preload heizt Ollama vor (keep_alive 30 m, num_predict 1);
  Pipeline ruft Preload vor summarize, damit Modell-Reload nicht mitten
  in der Generierung passiert (Hauptursache "Server disconnected")

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 09:12:27 +00:00

653 lines
16 KiB
CSS
Raw 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.

:root {
--bg: #0f1115;
--card: #181c24;
--border: #262b36;
--text: #e6e8eb;
--muted: #8b93a3;
--accent: #4f8cff;
--accent-hover: #3a78f0;
--ok: #2ecc71;
--warn: #f1c40f;
--err: #e74c3c;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.5;
}
header {
padding: 32px 24px 16px;
text-align: center;
border-bottom: 1px solid var(--border);
}
header h1 { margin: 0 0 4px; font-size: 28px; letter-spacing: .3px; }
.sub { color: var(--muted); margin: 0 0 12px; }
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 999px;
background: var(--card);
border: 1px solid var(--border);
font-size: 12px;
color: var(--muted);
}
.badge.ok { color: var(--ok); border-color: rgba(46,204,113,.4); }
.badge.err { color: var(--err); border-color: rgba(231,76,60,.4); }
main {
max-width: 1000px;
margin: 24px auto;
padding: 0 16px;
display: grid;
gap: 20px;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px 22px;
}
.card h2 { margin-top: 0; font-size: 18px; }
form { display: grid; gap: 12px; }
label { display: grid; gap: 6px; font-size: 14px; color: var(--muted); }
input[type=text], input[type=file] {
background: #0f1219;
border: 1px solid var(--border);
color: var(--text);
padding: 10px 12px;
border-radius: 8px;
font-size: 14px;
}
button {
background: var(--accent);
color: white;
border: 0;
padding: 10px 16px;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
justify-self: start;
}
button:hover { background: var(--accent-hover); }
button:disabled { opacity: .6; cursor: not-allowed; }
.hidden { display: none !important; }
.msg { min-height: 1em; margin: 8px 0 0; font-size: 13px; }
.msg.ok { color: var(--ok); }
.msg.err { color: var(--err); }
.muted { color: var(--muted); }
.bar {
width: 100%;
height: 8px;
background: #0f1219;
border-radius: 4px;
overflow: hidden;
margin-top: 6px;
}
.bar-fill {
height: 100%;
width: 0%;
background: var(--accent);
transition: width .25s ease;
}
table { width: 100%; border-collapse: collapse; font-size: 14px; table-layout: fixed; }
th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid var(--border); vertical-align: top; }
th { color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .5px; }
/* Spaltenbreiten — Status bleibt schmal, Downloads bekommen Platz */
.col-id { width: 56px; }
.col-status { width: 110px; }
.col-progress { width: 120px; }
.col-updated { width: 120px; }
.col-dl { width: 320px; }
/* col-title nimmt den Rest */
.col-title { word-break: break-word; }
.status-pill {
display: inline-block;
padding: 2px 10px;
border-radius: 999px;
font-size: 12px;
border: 1px solid var(--border);
white-space: nowrap;
}
.status-queued { color: var(--muted); }
.status-transcribing, .status-summarizing, .status-protocolling, .status-exporting { color: var(--warn); border-color: rgba(241,196,15,.4); }
.status-done { color: var(--ok); border-color: rgba(46,204,113,.4); }
.status-failed { color: var(--err); border-color: rgba(231,76,60,.4); }
/* Fehlertext unter dem Pill — bricht um, max. 3 Zeilen */
.status-error {
margin-top: 6px;
font-size: 11px;
color: var(--err);
line-height: 1.35;
word-break: break-word;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Download-Buttons — 3-spaltiges Grid (5 Buttons → 2 Zeilen 3+2) */
.dl {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 6px;
}
.btn-dl {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 5px 10px;
border-radius: 6px;
background: rgba(79,140,255,.12);
border: 1px solid rgba(79,140,255,.4);
color: var(--accent);
text-decoration: none;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
transition: background .15s ease, border-color .15s ease;
}
.btn-dl:hover { background: rgba(79,140,255,.22); border-color: var(--accent); }
.btn-dl.disabled {
color: var(--muted);
background: transparent;
border-color: var(--border);
pointer-events: none;
opacity: .55;
}
footer { text-align: center; padding: 24px 16px 32px; }
/* ── Header-Layout ─────────────────────────────────────────────── */
.header-row {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
max-width: 1000px;
margin: 0 auto;
}
.header-left { text-align: left; }
.header-right { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
.user-bar {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.user-bar strong { color: var(--text); }
.btn-link {
background: transparent;
color: var(--accent);
border: 0;
padding: 2px 6px;
font-size: 13px;
cursor: pointer;
text-decoration: underline;
}
.btn-link:hover { color: var(--accent-hover); }
.btn-link.btn-danger { color: var(--err); }
.admin-badge {
background: rgba(79,140,255,.18);
color: var(--accent);
border-color: rgba(79,140,255,.45);
text-transform: uppercase;
font-size: 11px;
letter-spacing: .5px;
padding: 2px 8px;
}
/* ── Auth-Overlay ──────────────────────────────────────────────── */
.overlay {
position: fixed;
inset: 0;
background: rgba(15, 17, 21, .92);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
padding: 20px;
}
.auth-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 28px 28px 22px;
width: 100%;
max-width: 380px;
box-shadow: 0 20px 60px rgba(0,0,0,.4);
}
.auth-card h2 { margin: 0 0 8px; font-size: 20px; }
.auth-card #auth-info { font-size: 13px; margin: 0 0 14px; }
.auth-card form { display: grid; gap: 12px; }
.auth-card button { justify-self: stretch; }
/* ── Admin: Benutzer-Verwaltung ────────────────────────────────── */
.user-form {
display: grid;
grid-template-columns: 1fr 1fr auto auto;
gap: 12px;
align-items: end;
}
.user-form label { font-size: 13px; }
.inline-check {
display: flex !important;
flex-direction: row !important;
align-items: center;
gap: 6px;
color: var(--text);
}
.inline-check input { margin: 0; }
#users-table .user-actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
@media (max-width: 700px) {
.user-form { grid-template-columns: 1fr; }
}
/* Select-Felder im selben Stil wie Inputs */
select {
background: #0f1219;
border: 1px solid var(--border);
color: var(--text);
padding: 10px 12px;
border-radius: 8px;
font-size: 14px;
font-family: inherit;
}
/* Kompakter Profil-Selector im Header */
.header-profile-wrap {
display: inline-flex;
align-items: center;
gap: 6px;
}
.header-profile {
padding: 3px 6px;
font-size: 12px;
border-radius: 5px;
background: #0f1219;
border: 1px solid var(--border);
color: var(--text);
max-width: 180px;
}
.header-profile:disabled { opacity: .5; }
/* Aufnahme-Button (groß, rot, touch-tauglich) */
.btn-record {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 14px 18px;
background: rgba(231,76,60,.12);
color: var(--err);
border: 1px solid rgba(231,76,60,.5);
border-radius: 10px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
min-height: 48px;
}
.btn-record:hover { background: rgba(231,76,60,.22); }
.rec-dot {
width: 12px; height: 12px;
background: var(--err);
border-radius: 50%;
}
/* Aufnahme-Bühne */
.record-pane {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 20px;
border: 1px solid rgba(231,76,60,.4);
background: rgba(231,76,60,.06);
border-radius: 12px;
margin-top: 6px;
}
.record-timer {
font-size: 44px;
font-variant-numeric: tabular-nums;
font-weight: 600;
color: var(--err);
letter-spacing: .04em;
}
.record-status { color: var(--muted); font-size: 13px; }
.record-status::before {
content: "● ";
color: var(--err);
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse { 50% { opacity: .25; } }
.record-controls { display: flex; gap: 10px; margin-top: 6px; }
.btn-record-ctrl {
padding: 12px 22px;
border-radius: 8px;
background: var(--card);
color: var(--text);
border: 1px solid var(--border);
font-size: 15px;
cursor: pointer;
min-height: 44px;
min-width: 100px;
}
.btn-record-ctrl.danger {
background: var(--err);
color: white;
border-color: var(--err);
}
/* Aufnahme-Ergebnis */
.record-result {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: rgba(46,204,113,.08);
border: 1px solid rgba(46,204,113,.4);
border-radius: 8px;
font-size: 13px;
}
/* ── Sichtbarkeit-Helfer ───────────────────────────────────────── */
.mobile-only { display: none; }
/* Upload-Form Layout (Desktop) */
.upload-meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.audio-file-details summary {
cursor: pointer;
padding: 8px 4px;
color: var(--muted);
font-size: 13px;
user-select: none;
}
.audio-file-details summary:hover { color: var(--text); }
.audio-file-details[open] summary { color: var(--text); margin-bottom: 8px; }
.audio-file-label {
display: grid;
gap: 6px;
}
/* Admin-Toggle (auf Desktop deaktiviert) */
.admin-toggle { cursor: default; }
.admin-toggle-icon { display: none; }
/* ── Mobile-Optimierung (echte Mobile-First-UI) ────────────────── */
@media (max-width: 700px) {
.desktop-only { display: none !important; }
.mobile-only { display: block; }
body { font-size: 15px; }
/* HEADER: nur Logo + User */
header {
padding: 10px 14px;
text-align: left;
border-bottom: 1px solid var(--border);
}
header h1 { font-size: 18px; margin: 0; }
.header-row {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.header-left, .header-right { align-items: center; }
.header-right { flex-direction: row; gap: 8px; flex: 1; justify-content: flex-end; }
.user-bar {
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
font-size: 13px;
flex-wrap: nowrap;
}
.user-bar strong { font-size: 14px; max-width: 90px; overflow: hidden; text-overflow: ellipsis; }
.header-profile { max-width: 130px; padding: 4px 6px; font-size: 12px; }
.btn-link { font-size: 12px; padding: 4px 6px; }
main { padding: 14px 12px 90px; gap: 14px; }
.card { padding: 16px; border-radius: 12px; }
.card h2 { font-size: 16px; margin-bottom: 10px; }
/* UPLOAD-CARD: Recording-Hero-Style */
.upload-card form { display: flex; flex-direction: column; gap: 14px; }
.btn-record {
/* HERO: groß und auffällig */
padding: 26px;
font-size: 19px;
min-height: 90px;
border-radius: 14px;
box-shadow: 0 4px 20px rgba(231,76,60,.15);
}
.btn-record .rec-dot { width: 16px; height: 16px; animation: pulse 1.5s ease-in-out infinite; }
.upload-meta { grid-template-columns: 1fr; gap: 10px; }
.upload-meta select,
.upload-meta input[type=text] {
padding: 14px 12px;
font-size: 16px; /* iOS: ≥16px verhindert Zoom-on-Focus */
min-height: 48px;
}
.audio-file-details summary { padding: 12px 4px; font-size: 14px; }
button[type="submit"] {
padding: 16px;
font-size: 16px;
min-height: 52px;
border-radius: 10px;
width: 100%;
}
/* Aufnahme-Bühne: noch größer auf Mobile */
.record-pane { padding: 24px 16px; }
.record-timer { font-size: 64px; line-height: 1; }
.record-controls { width: 100%; gap: 12px; }
.btn-record-ctrl {
flex: 1;
padding: 18px;
font-size: 17px;
min-height: 56px;
min-width: 0;
}
/* JOBS-CARDS statt Tabelle */
.jobs-cards { display: flex; flex-direction: column; gap: 10px; }
.jc {
background: #0f1219;
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px 14px;
}
.jc-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
margin-bottom: 6px;
}
.jc-id { color: var(--muted); font-size: 12px; }
.jc-title { font-weight: 600; font-size: 15px; line-height: 1.3; word-break: break-word; }
.jc-sub { color: var(--muted); font-size: 12px; margin-top: 2px; }
.jc-status-row {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0 6px;
}
.jc-progress { flex: 1; }
.jc-error {
color: var(--err);
font-size: 12px;
margin: 4px 0;
word-break: break-word;
}
.jc-dl {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-top: 8px;
}
.jc-dl .btn-dl { padding: 8px; font-size: 12px; min-height: 36px; }
/* Admin-Bereich: collapsible */
.admin-collapsible .admin-toggle {
cursor: pointer;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
margin: 0;
}
.admin-toggle-icon { display: inline; transition: transform .2s; }
.admin-collapsible:not(.open) .admin-body { display: none; }
.admin-collapsible.open .admin-toggle-icon { transform: rotate(180deg); }
.user-form { grid-template-columns: 1fr; }
.user-form input { font-size: 16px; }
}
/* iOS PWA: oberer Sicherheitsbereich (Notch) respektieren */
@supports (padding: max(0px)) {
header { padding-top: max(10px, env(safe-area-inset-top)); }
main { padding-bottom: max(20px, env(safe-area-inset-bottom)); }
}
/* Retry-Button in der Status-Spalte */
.btn-retry {
display: inline-block;
margin-top: 6px;
padding: 3px 8px;
font-size: 11px;
font-weight: 500;
background: rgba(241,196,15,.12);
color: var(--warn);
border: 1px solid rgba(241,196,15,.4);
border-radius: 5px;
text-decoration: none;
cursor: pointer;
}
.btn-retry:hover { background: rgba(241,196,15,.22); text-decoration: none; }
.btn-retry:disabled { opacity: .55; cursor: not-allowed; }
/* Profile-Tag in der Job-Zeile */
.tag-profile {
display: inline-block;
padding: 1px 8px;
margin-left: 6px;
border-radius: 4px;
background: rgba(46,204,113,.12);
border: 1px solid rgba(46,204,113,.4);
color: var(--ok);
font-size: 11px;
text-transform: lowercase;
vertical-align: middle;
}
/* Live-Timing direkt unter dem Status-Pill (läuft seit · letzter Ping vor) */
.status-timing, .jc-timing {
margin-top: 4px;
font-size: 11px;
line-height: 1.3;
}
.live-timing { color: var(--muted); }
/* Diagnose-Button wie btn-retry, aber neutraler Ton */
.btn-diag {
display: inline-block;
margin-top: 6px;
margin-left: 4px;
padding: 4px 10px;
font-size: 12px;
background: rgba(120,120,120,.10);
border: 1px solid rgba(160,160,160,.35);
color: var(--muted);
border-radius: 5px;
text-decoration: none;
cursor: pointer;
}
.btn-diag:hover { background: rgba(160,160,160,.20); color: inherit; }
/* Aktions-Reihe in der mobilen Karte */
.jc-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
/* Diagnose-Panel: Tabelle (zweite Zeile, full-width) und Karte */
.diag-row > td {
background: rgba(255,255,255,.03);
border-top: 0;
padding: 10px 12px;
}
.diag-panel {
margin-top: 10px;
padding: 10px;
background: rgba(255,255,255,.03);
border: 1px solid rgba(255,255,255,.08);
border-radius: 6px;
}
.diag-meta {
display: flex;
gap: 16px;
flex-wrap: wrap;
font-size: 12px;
color: var(--muted);
margin-bottom: 6px;
}
.diag-meta code {
background: rgba(255,255,255,.08);
padding: 1px 4px;
border-radius: 3px;
}
.diag-err {
margin: 6px 0;
padding: 6px 8px;
background: rgba(231,76,60,.10);
border: 1px solid rgba(231,76,60,.35);
border-radius: 4px;
color: var(--err);
font-size: 12px;
word-break: break-word;
}
.diag-loghead {
margin-top: 8px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: .05em;
color: var(--muted);
}
.diag-log {
margin: 4px 0 0 0;
padding: 8px;
background: rgba(0,0,0,.30);
border: 1px solid rgba(255,255,255,.06);
border-radius: 4px;
max-height: 320px;
overflow: auto;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 11.5px;
line-height: 1.45;
white-space: pre-wrap;
word-break: break-word;
}