From 63c7d30b5ecf134d782ccb675a549e5020774c7b Mon Sep 17 00:00:00 2001 From: schakal00783 Date: Fri, 17 Jul 2026 00:34:23 +0200 Subject: [PATCH] GUI: Weggezogen-Bug (stale detail poll) gefixt + Redesign mit Dark/Light-Switch - Detail-Poll bricht bei Auswahlwechsel ab, pollt nur Status, EIN Rebuild bei Fertigstellung mit Player-Restore - Themes via CSS-Variablen (data-theme, localStorage, OS-Default), Kupfer-Akzent, Wellenform-Wortmarke, Pill-Tabs, focus-visible, prefers-reduced-motion Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV --- app/web/index.html | 9 ++ app/web/src/main.js | 48 +++++++- app/web/src/style.css | 275 +++++++++++++++++++++++++++++------------- docs/08-changelog.md | 15 +++ 4 files changed, 258 insertions(+), 89 deletions(-) diff --git a/app/web/index.html b/app/web/index.html index 8123f68..881ab91 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -4,6 +4,15 @@ Aime +
diff --git a/app/web/src/main.js b/app/web/src/main.js index 816a674..f9525e8 100644 --- a/app/web/src/main.js +++ b/app/web/src/main.js @@ -13,18 +13,34 @@ const state = { tab: 'record', recording: false, recStart: 0, timer: null, selec function fmt(ms) { const s = Math.floor(ms / 1000); return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`; } function toast(msg, err) { const t = document.createElement('div'); t.className = 'toast' + (err ? ' err' : ''); t.textContent = msg; document.body.appendChild(t); setTimeout(() => t.remove(), 4000); } +// Theme: data-theme on , chosen in index.html before first paint (no +// flash); the toggle persists to localStorage. +function applyTheme(t) { + document.documentElement.dataset.theme = t; + localStorage.setItem('aime-theme', t); + const b = document.querySelector('#themetoggle'); + if (b) b.textContent = t === 'dark' ? '☀️' : '🌙'; +} + function render() { app.classList.toggle('wide', state.tab === 'recordings'); + const theme = document.documentElement.dataset.theme || 'dark'; app.innerHTML = ` -
-

📞 Aime Anruf-Assistent · RK3588S

+
+
+ +

Aime Anruf-Assistent

+
+
`; app.querySelectorAll('.tab').forEach(b => b.onclick = () => { state.tab = b.dataset.tab; render(); }); + app.querySelector('#themetoggle').onclick = () => + applyTheme(document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'); ({ record: viewRecord, bluetooth: viewBluetooth, recordings: viewRecordings, voices: viewVoices, glossary: viewGlossary })[state.tab](); } @@ -417,8 +433,32 @@ async function openRecording(id) { catch (e) { toast(e.message, true); rtBtn.disabled = false; } }; ensureAnaTicker(); - // Poll while transcription OR the NPU analysis is still running, then refresh. - if (pending || transcribing) setTimeout(() => { const d = document.querySelector('#detail'); if (d && d.querySelector('.analysis')) openRecording(id); }, 5000); + // Poll while transcription OR the NPU analysis is still running. The old + // version re-ran openRecording(id) blindly every 5s — a stale closure: after + // switching to ANOTHER recording the timer still fired and yanked the user + // (and the playing audio) back to the analysing one. Now: stop when the + // selection changed, poll ONLY the status while busy (no DOM rebuild), and + // rebuild once when the results are in — restoring the player position. + if (pending || transcribing) { + const pollDetail = async () => { + if (state.selectedId !== id || state.tab !== 'recordings') return; // user moved on + if (!document.querySelector('#detail')) return; // view gone + let cur; + try { cur = await api.get('/api/recordings/' + id); } catch { return; } + if (state.selectedId !== id || state.tab !== 'recordings') return; // re-check after await + const busy = cur.analysis_status === 'pending' || cur.status === 'transcribing'; + if (busy) { setTimeout(pollDetail, 5000); return; } + // Results arrived → one rebuild; keep the audio where the user had it. + const a = document.querySelector('#detail #player'); + const keep = a && !a.paused ? { t: a.currentTime, play: true } : (a ? { t: a.currentTime, play: false } : null); + await openRecording(id); + if (keep && keep.t > 0) { + const na = document.querySelector('#detail #player'); + if (na) { na.currentTime = keep.t; if (keep.play) na.play().catch(() => {}); } + } + }; + setTimeout(pollDetail, 5000); + } const audio = detail.querySelector('#player'); const spans = [...detail.querySelectorAll('.w')]; spans.forEach(s => s.onclick = () => { audio.currentTime = (+s.dataset.start) / 1000 + 0.001; audio.play(); }); diff --git a/app/web/src/style.css b/app/web/src/style.css index d73ed94..02c0243 100644 --- a/app/web/src/style.css +++ b/app/web/src/style.css @@ -1,106 +1,211 @@ -:root { - --bg: #0e1116; --card: #171c24; --line: #262d38; --fg: #e7edf5; --muted: #8a97a8; - --accent: #4f8cff; --ok: #2ecc71; --warn: #f0b429; --danger: #ff5b5b; +/* Aime — Anruf-Assistent. Zwei Themes über CSS-Variablen; data-theme setzt + index.html vor dem ersten Paint, der Header-Toggle schaltet um. + Akzent = warmes Kupfer (Tonband-Anmutung), Rot bleibt der Aufnahme reserviert. */ + +:root[data-theme="dark"] { + --bg: #14161b; + --surface: #1d222a; + --surface-2: #262c36; + --inset: #10141a; + --line: #303845; + --fg: #e9ecf1; + --muted: #98a2b3; + --accent: #e8804c; + --accent-fg: #17130f; + --accent-soft: rgba(232, 128, 76, .14); + --ok: #55b381; + --ok-soft: rgba(85, 179, 129, .14); + --warn: #e2b93b; + --warn-soft: rgba(226, 185, 59, .14); + --danger: #e5595e; + --danger-soft: rgba(229, 89, 94, .12); + --shadow: none; + --sp0: #5bc0eb; --sp1: #ef8fc0; --sp2: #a8cc4e; --sp3: #e6b84c; + --sp0-w: #8fd6f5; --sp1-w: #f5b5d6; --sp2-w: #c5e07e; --sp3-w: #f0d08a; } +:root[data-theme="light"] { + --bg: #f4f2ee; + --surface: #ffffff; + --surface-2: #eceae4; + --inset: #f7f6f2; + --line: #ddd8cf; + --fg: #23272e; + --muted: #6d7683; + --accent: #c05f2d; + --accent-fg: #ffffff; + --accent-soft: rgba(192, 95, 45, .1); + --ok: #2f8a5d; + --ok-soft: rgba(47, 138, 93, .12); + --warn: #9a7514; + --warn-soft: rgba(154, 117, 20, .12); + --danger: #c73e43; + --danger-soft: rgba(199, 62, 67, .09); + --shadow: 0 1px 3px rgba(35, 39, 46, .07); + --sp0: #1179a8; --sp1: #c2417f; --sp2: #5f7d16; --sp3: #a4700d; + --sp0-w: #0e6b95; --sp1-w: #ad3a72; --sp2-w: #556f14; --sp3-w: #92640b; +} + * { box-sizing: border-box; } -body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.5 system-ui, "Segoe UI", Roboto, sans-serif; } +html { color-scheme: dark; } +:root[data-theme="light"] { color-scheme: light; } +body { + margin: 0; background: var(--bg); color: var(--fg); + font: 15px/1.5 system-ui, "Segoe UI", Roboto, sans-serif; + transition: background .2s, color .2s; +} #app { max-width: 860px; margin: 0 auto; padding: 16px; } #app.wide { max-width: 1180px; } -header h1 { margin: 0 0 4px; font-size: 22px; } -header .sub { font-size: 12px; color: var(--muted); font-weight: 400; } -nav { display: flex; gap: 8px; margin: 14px 0; } -.tab { background: var(--card); color: var(--muted); border: 1px solid var(--line); padding: 8px 16px; border-radius: 10px; cursor: pointer; font-size: 14px; } -.tab.active { color: #fff; border-color: var(--accent); background: #1c2735; } -.card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 18px; margin-bottom: 16px; } + +/* ---- Topbar / Brand ---- */ +.topbar { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; padding: 6px 0 14px; } +.brand { display: flex; align-items: center; gap: 10px; } +.brand-wave { width: 30px; height: 18px; color: var(--accent); flex: none; } +.topbar h1 { margin: 0; font-size: 21px; font-weight: 650; letter-spacing: -.01em; } +.topbar .sub { font-size: 12px; color: var(--muted); font-weight: 400; margin-left: 6px; } +nav { display: flex; gap: 6px; flex-wrap: wrap; } +.topbar nav { margin-left: auto; } +.tab { + background: transparent; color: var(--muted); border: 1px solid transparent; + padding: 7px 14px; border-radius: 999px; cursor: pointer; font-size: 14px; + transition: color .12s, background .12s; +} +.tab:hover { color: var(--fg); background: var(--surface-2); } +.tab.active { color: var(--accent); background: var(--accent-soft); font-weight: 600; } +.btn.icon.theme { border-radius: 999px; font-size: 15px; line-height: 1; } + +/* ---- Bausteine ---- */ +.card { + background: var(--surface); border: 1px solid var(--line); border-radius: 14px; + padding: 18px; margin-bottom: 16px; box-shadow: var(--shadow); +} +h2 { margin: 0 0 12px; font-size: 18px; letter-spacing: -.01em; } .row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; } .grow { flex: 1; } -.btn { background: #222a35; color: var(--fg); border: 1px solid var(--line); border-radius: 9px; padding: 8px 14px; cursor: pointer; font-size: 14px; } +.btn { + background: var(--surface-2); color: var(--fg); border: 1px solid var(--line); + border-radius: 9px; padding: 8px 14px; cursor: pointer; font-size: 14px; + transition: border-color .12s, background .12s; +} .btn:hover { border-color: var(--accent); } -.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; } -.btn.danger { background: #3a2224; border-color: #5a2f31; color: var(--danger); } +.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); font-weight: 600; } +.btn.primary:hover { filter: brightness(1.07); } +.btn.danger { background: var(--danger-soft); border-color: transparent; color: var(--danger); } +.btn.danger:hover { border-color: var(--danger); } .btn.big { font-size: 17px; padding: 14px 22px; border-radius: 12px; } -.btn:disabled { opacity: .6; cursor: default; } +.btn.small { font-size: .8rem; padding: 3px 8px; } +.btn.icon { padding: 6px 9px; flex: none; } +.btn:disabled { opacity: .55; cursor: default; } +:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } .lbl { display: block; font-size: 12px; color: var(--muted); margin: 10px 0 4px; text-transform: uppercase; letter-spacing: .04em; } -.input, select { width: 100%; background: #0f141b; color: var(--fg); border: 1px solid var(--line); border-radius: 9px; padding: 9px 12px; font-size: 14px; } +.input, select { + width: 100%; background: var(--inset); color: var(--fg); + border: 1px solid var(--line); border-radius: 9px; padding: 9px 12px; font-size: 14px; +} +.input:focus, select:focus { border-color: var(--accent); outline: none; } .list { display: flex; flex-direction: column; gap: 8px; } -.item { display: flex; gap: 10px; align-items: center; background: #0f141b; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; } -.badge { font-size: 11px; padding: 3px 8px; border-radius: 20px; background: #222a35; color: var(--muted); } -.badge.ok { background: #14351f; color: var(--ok); } -.badge.warn { background: #352c14; color: var(--warn); } +.item { + display: flex; gap: 10px; align-items: center; + background: var(--inset); border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; +} +.badge { font-size: 11px; padding: 3px 8px; border-radius: 20px; background: var(--surface-2); color: var(--muted); } +.badge.ok { background: var(--ok-soft); color: var(--ok); } +.badge.warn { background: var(--warn-soft); color: var(--warn); } .muted { color: var(--muted); } .mono { font-family: ui-monospace, monospace; font-size: 12px; } .err { color: var(--danger); } +.err-inline { color: var(--danger); margin-bottom: 6px; } +.hint { font-size: .8rem; color: var(--muted); margin-bottom: 10px; } + +/* ---- Aufnahme ---- */ .rec-panel { display: flex; flex-direction: column; align-items: center; gap: 14px; margin: 22px 0 8px; } .timer { font-size: 40px; font-variant-numeric: tabular-nums; font-weight: 600; } -h2 { margin: 0 0 12px; } audio { width: 100%; margin-bottom: 10px; } -.hint { font-size: 12px; color: var(--muted); margin-bottom: 10px; } + +/* ---- Transkript ---- */ .transcript { line-height: 2.1; font-size: 16px; } .transcript .w { cursor: pointer; padding: 1px 2px; border-radius: 4px; transition: background .1s; } -.transcript .w:hover { background: #24303f; } -.transcript .w.active { background: var(--accent); color: #fff; } -.toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #1c2735; border: 1px solid var(--accent); color: #fff; padding: 10px 18px; border-radius: 10px; z-index: 99; max-width: 90%; } +.transcript .w:hover { background: var(--surface-2); } +.transcript .w.active, .w.active { background: var(--accent); color: var(--accent-fg); } +.w.sp0 { color: var(--sp0-w); } .w.sp1 { color: var(--sp1-w); } +.w.sp2 { color: var(--sp2-w); } .w.sp3 { color: var(--sp3-w); } + +.toast { + position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); + background: var(--surface); border: 1px solid var(--accent); color: var(--fg); + padding: 10px 18px; border-radius: 10px; z-index: 99; max-width: 90%; box-shadow: var(--shadow); +} .toast.err { border-color: var(--danger); } -/* LLM analysis */ -.summary-snip{font-size:.85rem;color:#9fb3c8;margin-top:4px;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical} -.analysis{margin:10px 0;padding:10px;background:#111a24;border-radius:8px} -.analysis h3{margin:8px 0 4px;font-size:.9rem;color:#cbd5e1} -.summary{margin:0 0 6px;line-height:1.5;white-space:pre-line} -.todos{margin:0;padding-left:20px} -.todos li{margin:3px 0} -.err-inline{color:#f87171;margin-bottom:6px} -.btn.small{font-size:.8rem;padding:3px 8px} -.hint{font-size:.8rem;color:#8aa} - -.parts{margin:0;padding-left:20px}.parts li{margin:3px 0} -.clean{line-height:1.55;background:#0e1620;padding:8px 10px;border-radius:8px;margin:0 0 4px} - -.dialog{display:flex;flex-direction:column;gap:6px;margin-bottom:4px} -.turn{padding:6px 10px;border-radius:8px;background:#0e1620;line-height:1.5} -.turn .spk{margin-right:6px} -.turn.sp0{border-left:3px solid #38bdf8}.turn.sp0 .spk{color:#38bdf8} -.turn.sp1{border-left:3px solid #f472b6}.turn.sp1 .spk{color:#f472b6} -.turn.sp2{border-left:3px solid #a3e635}.turn.sp2 .spk{color:#a3e635} -.turn.sp3{border-left:3px solid #fbbf24}.turn.sp3 .spk{color:#fbbf24} - -.w.sp0{color:#7dd3fc}.w.sp1{color:#f9a8d4}.w.sp2{color:#bef264}.w.sp3{color:#fcd34d} -.w.active{background:#334155;color:#fff} -/* speaker name editor (rename + learn voiceprint) */ -.spk-edit{display:flex;flex-direction:column;gap:6px;margin:4px 0 8px} -.spk-row{display:flex;align-items:center;gap:8px;padding:4px 8px;border-radius:8px;background:#0e1620;border-left:3px solid #334155} -.spk-row.sp0{border-left-color:#38bdf8}.spk-row.sp1{border-left-color:#f472b6}.spk-row.sp2{border-left-color:#a3e635}.spk-row.sp3{border-left-color:#fbbf24} -.spk-dot{width:10px;height:10px;border-radius:50%;background:#64748b;flex:none} -.spk-row.sp0 .spk-dot{background:#38bdf8}.spk-row.sp1 .spk-dot{background:#f472b6}.spk-row.sp2 .spk-dot{background:#a3e635}.spk-row.sp3 .spk-dot{background:#fbbf24} -.spk-name{flex:1;min-width:80px;padding:5px 8px;border-radius:6px;border:1px solid #334155;background:#0b1220;color:#e2e8f0} -.spk-learn-l{display:flex;align-items:center;gap:4px;font-size:12px;color:#94a3b8;white-space:nowrap} - -.ana-time{font-variant-numeric:tabular-nums;font-weight:600} -.ana-time.live{color:#4ea1ff} -.ana-line{margin:2px 0 8px} - -/* ---- Recordings split layout: list (left) + detail (right) ---- */ -.rec-layout{display:flex;gap:14px;align-items:flex-start} -.rec-list-pane{flex:0 0 340px;position:sticky;top:12px;max-height:calc(100vh - 90px);overflow-y:auto;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:10px} -.rec-detail-pane{flex:1 1 auto;min-width:0} -.rec-detail-pane .card{margin-bottom:0} -.detail-empty{padding:40px 16px;text-align:center;border:1px dashed var(--line);border-radius:14px} -/* date groups */ -.day-group{margin-bottom:6px} -.day-head{display:flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.03em;padding:6px 4px;user-select:none} -.day-head:hover{color:var(--fg)} -.day-caret{display:inline-block;transition:transform .15s;font-size:10px} -.day-group.collapsed .day-caret{transform:rotate(-90deg)} -.day-group.collapsed .day-items{display:none} -.day-count{margin-left:auto;background:#222a35;border-radius:20px;padding:1px 8px;font-size:11px} -.day-items{display:flex;flex-direction:column;gap:6px} -/* list rows become clickable cards; selected row highlighted */ -.item.rec{cursor:pointer;transition:border-color .1s,background .1s} -.item.rec:hover{border-color:var(--accent)} -.item.rec.sel{border-color:var(--accent);background:#1c2735} -.btn.icon{padding:6px 9px;flex:none} -@media(max-width:800px){ - #app.wide{max-width:860px} - .rec-layout{flex-direction:column} - .rec-list-pane{flex:none;width:100%;position:static;max-height:340px} +/* ---- Analyse ---- */ +.summary-snip { + font-size: .85rem; color: var(--muted); margin-top: 4px; overflow: hidden; + text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; +} +.analysis { margin: 10px 0; padding: 12px; background: var(--inset); border: 1px solid var(--line); border-radius: 10px; } +.analysis h3 { margin: 8px 0 4px; font-size: .9rem; color: var(--muted); } +.summary { margin: 0 0 6px; line-height: 1.55; white-space: pre-line; } +.todos { margin: 0; padding-left: 20px; } +.todos li { margin: 3px 0; } +.parts { margin: 0; padding-left: 20px; } .parts li { margin: 3px 0; } +.clean { line-height: 1.55; background: var(--inset); padding: 8px 10px; border-radius: 8px; margin: 0 0 4px; } + +/* ---- Dialog-Turns / Sprecher ---- */ +.dialog { display: flex; flex-direction: column; gap: 6px; margin-bottom: 4px; } +.turn { padding: 6px 10px; border-radius: 8px; background: var(--inset); line-height: 1.5; } +.turn .spk { margin-right: 6px; font-weight: 600; } +.turn.sp0 { border-left: 3px solid var(--sp0); } .turn.sp0 .spk { color: var(--sp0); } +.turn.sp1 { border-left: 3px solid var(--sp1); } .turn.sp1 .spk { color: var(--sp1); } +.turn.sp2 { border-left: 3px solid var(--sp2); } .turn.sp2 .spk { color: var(--sp2); } +.turn.sp3 { border-left: 3px solid var(--sp3); } .turn.sp3 .spk { color: var(--sp3); } + +.spk-edit { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 8px; } +.spk-row { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-radius: 8px; background: var(--inset); border-left: 3px solid var(--line); } +.spk-row.sp0 { border-left-color: var(--sp0); } .spk-row.sp1 { border-left-color: var(--sp1); } +.spk-row.sp2 { border-left-color: var(--sp2); } .spk-row.sp3 { border-left-color: var(--sp3); } +.spk-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--muted); flex: none; } +.spk-row.sp0 .spk-dot { background: var(--sp0); } .spk-row.sp1 .spk-dot { background: var(--sp1); } +.spk-row.sp2 .spk-dot { background: var(--sp2); } .spk-row.sp3 .spk-dot { background: var(--sp3); } +.spk-name { flex: 1; min-width: 80px; padding: 5px 8px; border-radius: 6px; border: 1px solid var(--line); background: var(--surface); color: var(--fg); } +.spk-learn-l { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--muted); white-space: nowrap; } + +.ana-time { font-variant-numeric: tabular-nums; font-weight: 600; } +.ana-time.live { color: var(--accent); } +.ana-line { margin: 2px 0 8px; } + +/* ---- Aufnahmen: Liste links, Detail rechts ---- */ +.rec-layout { display: flex; gap: 14px; align-items: flex-start; } +.rec-list-pane { + flex: 0 0 340px; position: sticky; top: 12px; max-height: calc(100vh - 90px); + overflow-y: auto; background: var(--surface); border: 1px solid var(--line); + border-radius: 14px; padding: 10px; box-shadow: var(--shadow); +} +.rec-detail-pane { flex: 1 1 auto; min-width: 0; } +.rec-detail-pane .card { margin-bottom: 0; } +.detail-empty { padding: 40px 16px; text-align: center; border: 1px dashed var(--line); border-radius: 14px; } + +.day-group { margin-bottom: 6px; } +.day-head { + display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px; + color: var(--muted); text-transform: uppercase; letter-spacing: .03em; padding: 6px 4px; user-select: none; +} +.day-head:hover { color: var(--fg); } +.day-caret { display: inline-block; transition: transform .15s; font-size: 10px; } +.day-group.collapsed .day-caret { transform: rotate(-90deg); } +.day-group.collapsed .day-items { display: none; } +.day-count { margin-left: auto; background: var(--surface-2); border-radius: 20px; padding: 1px 8px; font-size: 11px; } +.day-items { display: flex; flex-direction: column; gap: 6px; } + +.item.rec { cursor: pointer; transition: border-color .1s, background .1s; } +.item.rec:hover { border-color: var(--accent); } +.item.rec.sel { border-color: var(--accent); background: var(--accent-soft); } + +@media (max-width: 800px) { + #app.wide { max-width: 860px; } + .rec-layout { flex-direction: column; } + .rec-list-pane { flex: none; width: 100%; position: static; max-height: 340px; } + .topbar nav { margin-left: 0; width: 100%; order: 3; } +} + +@media (prefers-reduced-motion: reduce) { + * { transition: none !important; animation: none !important; } } diff --git a/docs/08-changelog.md b/docs/08-changelog.md index ba8c325..7d0cd24 100644 --- a/docs/08-changelog.md +++ b/docs/08-changelog.md @@ -3,6 +3,21 @@ Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum, Was, Warum, betroffene Dateien/Backups). +## 2026-07-17 — GUI: „Weggezogen"-Bug gefixt + Redesign mit Dark/Light-Switch (`.bak-theme`) + +- **Bug:** lief beim ausgewählten Eintrag eine Analyse, feuerte ein 5-s-Timer `openRecording(alteId)` als + **stale closure** — auch nachdem der Nutzer längst eine andere Aufnahme hörte → riss ihn zurück und + stoppte den Player. Fix: Poll bricht bei Auswahl-/Tab-Wechsel ab, prüft während der Analyse NUR den + Status (kein DOM-Rebuild) und baut das Detail genau EINMAL neu, wenn die Ergebnisse da sind — inkl. + Wiederherstellung von Player-Position und Play-Zustand. +- **Redesign:** Themes über CSS-Variablen (`data-theme` auf ``, vor First-Paint aus localStorage/ + OS-Einstellung, Toggle ☀️/🌙 im Header). Dunkel = neutrales Anthrazit, Hell = warmes Papier; Akzent = + Kupfer (#e8804c/#c05f2d — Rot bleibt der Aufnahme vorbehalten), Wortmarke mit Wellenform-Glyph, + Pill-Tabs, weichere Karten, sichtbarer Tastatur-Fokus, `prefers-reduced-motion` respektiert. + Sprecherfarben (sp0–sp3) als Variablen mit hell/dunkel abgestimmten Werten. +- Bundle `index-C2xK4eql.js` / `index-ClT74fBX.css`. Dateien: `web/index.html`, `web/src/main.js`, + `web/src/style.css` (Backups `.bak-theme`). + ## 2026-07-17 — Hall auf der Gegenseite + verschmolzene Sprecher: Mikro→Mix jetzt GEGATET (`.bak-gatedmix`) User: Gegenseite klingt „wie in einem schallenden Raum", Diarization macht aus zwei entfernten Personen