# 08 — Changelog Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum, Was, Warum, betroffene Dateien/Backups). ## 2026-08-20 — Stop-Knopf reagiert sofort (MP3-Encode in den Hintergrund) **Meldung:** „Wenn ich eine laufende Aufnahme stoppen will, passiert auf der GUI nichts. Ich muss den Reiter wechseln und zurück, dann kann ich nochmal auf Stopp klicken und dann geht's." **Gemessene Ursache:** `POST /api/record/stop` wartete auf den kompletten `loudnorm`-MP3-Encode — **~1,4 s pro aufgenommener Minute** (#114: 35,5 min → 50,9 s; #118: 30,8 min → 43,8 s; berechnet aus `wav_file`-Zeitstempel + `duration_ms` gegen `created_at`). Die GUI zeigte dabei nur einen ausgegrauten Knopf. Der Reiterwechsel löst `render()` aus, das den Knopf frisch und klickbar aufbaut, obwohl `state.recording` noch `true` ist → der zweite Klick schickte einen zweiten Stop, der an `not recording` scheiterte (`stopAndSave()` setzt `current` in seiner ersten Zeile auf `null`). **Nicht** die Kontextnotiz vom 2026-08-17: die Wartezeit gab es vorher genauso (#113 am 14.08.: 44,9 s). **Was geändert wurde** - `recorder.js`: `stopAndSave()` verschiebt das WAV und startet `encodeMp3()` **ohne `await`**. Der Encode schreibt nach `.mp3.part` und benennt erst danach um (nie eine halbe Datei im Player). `MP3_ARGS` bekam **`-f mp3`** — ohne das leitet ffmpeg den Muxer aus `.part` ab und bricht mit `Error initializing the muxer … Invalid argument` ab (erster Testlauf: MP3 fehlte komplett, beide Encode-Zweige waren still gescheitert). - `web/src/main.js`: `state.stopping` überlebt `render()` → Knopf bleibt über Reiterwechsel hinweg gesperrt (⏳ + „speichert…"), Uhr friert ein, Live-Karte sagt „Aufnahme wird gespeichert…", Toast nennt die geschätzte Wartezeit, zweiter Klick wird verworfen, `not recording` ist kein roter Fehler mehr. Dazu lädt der Player nach (`#mp3wait`, `error`-Ereignis → `audio.load()` alle 5 s, Abbruch nach 5 min). - `server.js`: `sweepOrphans` erkennt `rec_*.mp3.part`, damit eine beim Absturz liegengebliebene Halbdatei nach `ORPHAN_MIN_AGE_MS` mit weggeräumt wird. **Gegenprobe:** Stop antwortet in **0,033 s / 0,044 s** (12- und 15-s-Testclips #119/#120, danach in den Papierkorb), `mp3 fertig: …` erscheint ~2 s später im Journal, Datei ist 64 kbit CBR / 48 kHz / mono ohne Xing-Tabelle, `/media/…` liefert 200. **Dateien/Backups:** `recorder.js.bak-stopfast`, `server.js.bak-stopfast`, `main.js.bak-stopfix`, `main.js.bak-mp3wait`. Backend geändert ⇒ Neustart, vorher `analysis_status='pending'` geprüft (leer). Der Vite-Build wurde bis zum Ende der laufenden Analyse #118 aufgeschoben: bei 816 MB freiem RAM neben dem residenten rkllm (~6,5 GB) hätte er den OOM-Killer auf genau diese Analyse gezogen. Kapitel 03 aktualisiert (neuer Abschnitt „Der Stop-Knopf" mit Vorher/Nachher-Sequenzdiagramm). ## 2026-08-17 (Nachtrag 6) — Diagramme zur Systemanzeige Kapitel 07 nachgerüstet: ein Sequenzdiagramm für den Abfrageweg (GUI-Poll → `/api/sysstat` → `sysstat.js` → `/proc`/`statfs`, dazu der 60-s-Puffer des `recordings/`-Scans und der 2-s-Ticker für die CPU-Differenz) und ein Flussdiagramm für die beiden Rechenwege (Ring = Aufnahmen / (frei + Aufnahmen); Ventilator = Drehstufe aus CPU-%, Ampel aus dem schlimmsten Wert von CPU, RAM, CPU-Temperatur). Damit erfüllt die Änderung die Hausregel „neue Abläufe als Mermaid-Diagramm“. ## 2026-08-17 (Nachtrag 5) — „CPU-Temperatur“ statt Zonenname in Klammern Die Zeile heißt jetzt „CPU-Temperatur“ und zeigt nur noch `46,2 °C`; der Zonenname in Klammern (`littlecore-thermal` o. ä.) ist weg. Damit die Beschriftung auch stimmt, filtert `sysstat.js` die Zonen enger: nur `soc-thermal`, `bigcore0/1-thermal`, `littlecore-thermal` — GPU-, NPU- und Center-Zone fielen vorher unter „wärmste Zone“ und hätten den Wert stellen können, obwohl daran „CPU“ steht. Feld `temp.zone` bleibt in der API (fürs Debuggen), die GUI zeigt es nicht mehr. Backend geändert ⇒ Neustart nach Prüfung `pending=0`. ## 2026-08-17 (Nachtrag 4) — Erklärtext und Partitionszeile aus der Speicherkarte Endstand der Klappkarte: Titel, „Aufnahmen“ (GB + Dateizahl) mit Balken, „Noch frei“, „Gesamt für Aufnahmen“. Der Erklärtext zur 100-%-Bezugsgröße und die Fußzeile „Partition gesamt“ sind gestrichen — die Bezugsgröße ergibt sich aus den drei Zeilen selbst. `/api/sysstat` liefert `partPct`/`totalBytes` unverändert weiter, die GUI zeigt sie nur nicht mehr. Nur Frontend + Vite-Build. ## 2026-08-17 (Nachtrag 3) — Karte wieder vollständig, dafür umbruchfrei Der vermeintlich „fehlende Wert“ war ein **Zeilenumbruch**: bei „Gesamt für Aufnahmen 27,5 GB“ rutschte die Einheit allein in die nächste Zeile. Also alles wieder rein (Balken, „Noch frei“, „Gesamt für Aufnahmen“, Erklärtext, Partitions-Fußzeile) und stattdessen der Umbruch abgestellt: `.sys-row { white-space: nowrap }` + `.sys-pop-disk { min-width: 285px }`, der Erklärtext darf als einziger weiter umbrechen. Die in Nachtrag 2 entfernte Media-Query bleibt entfernt — die Prozentzahl am Ring war unter 900 px Fensterbreite ausgeblendet, das soll sie nicht sein. Nur Frontend (`main.js`, `style.css` + Vite-Build), kein Dienstneustart. ## 2026-08-17 (Nachtrag 2) — Speicherkarte kurzzeitig auf eine Zeile gekürzt Auf Zuruf alles unterhalb der GB-Zeile entfernt (Balken, „Noch frei“, „Gesamt für Aufnahmen“, Erklärtext, Partitions-Fußzeile) und die Media-Query `@media (max-width: 900px) { .sys-val { display: none } }` gestrichen, die bei schmalem Fenster die Prozentzahl neben dem Ring ausblendete. Die Kürzung wurde in Nachtrag 3 wieder zurückgenommen, das Entfernen der Media-Query bleibt. ## 2026-08-17 (Nachtrag) — Ventilator statt Turbine, Speicher relativ zum Aufnahme-Vorrat **Zwei Korrekturen des Nutzers am selben Tag:** 1. **Icon soll wie ein Ventilator aussehen.** Der erste Entwurf (fünf schmale Tropfen ohne Gehäuse) las sich als Kleeblatt. Jetzt: vier gebogene Pinwheel-Flügel in einem feststehenden Gehäusering; nur die Flügelgruppe dreht. Die Form wurde vor dem Ausrollen ohne Browser geprüft — die Bézier-Segmente der Kandidaten wurden mit einem kleinen Python/Pillow-Skript abgetastet, rotiert und als PNG gerendert (`lab/preview_fan.py`), weil die Chrome-Anbindung an diesem Tag nicht verfügbar war. 2. **Prozentzahl soll den Aufnahme-Vorrat messen, nicht die Partition.** Neue Bezugsgröße: `100 % = freier Platz + Summe aller Audiodateien unter recordings/`, belegt = die Audiodateien. Das ist genau der Platz, den ein frisch aufgesetztes System dieser Ausbaustufe für Aufnahmen hätte; System, Modelle und `node_modules` fallen aus Zähler **und** Nenner heraus. Der `df`-Wert bleibt als Fußzeile („Partition gesamt“) in der Klappkarte. **Umsetzung:** `sysstat.js` scannt `recordings/` mit rekursivem `readdir` + `stat` (nur Audio-Endungen, kein `du`-Prozess) und puffert das Ergebnis 60 s; nur der allererste Aufruf wartet auf den Scan, sonst zeigte die Anzeige direkt nach dem Start einmal 0 %. Neue Felder `recBytes`, `recFiles`, `budgetBytes`, `partPct`; `usedPct` = `recBytes / budgetBytes`. `status()` ist dadurch `async` → die Route läuft jetzt über `wrap()`. GUI: Balken + Datei-Anzahl in der Karte, erklärende Fußnote, Fußzeile mit dem Partitionswert. Gegenprobe: Scan meldet 1 957 445 371 Bytes, `du -sb` sagt exakt dasselbe (103 Dateien, 52 mp3 + 51 wav) ⇒ 1,96 GB von 29,6 GB Vorrat = **6,6 %** (Partition dagegen 52 %). ## 2026-08-17 — Systemanzeige in der Kopfzeile: Speicherring + Turbine **Wunsch:** zwischen dem Bluetooth-Tab und dem Hell/Dunkel-Schalter eine Anzeige, die den belegten Speicherplatz in Prozent zeigt, dazu ein zweites Symbol, das bei Mauskontakt CPU-Last, RAM und Temperatur aufklappt — als Turbine, die sich mit steigender Last schneller dreht. **Umsetzung:** - Neues Modul `sysstat.js` mit `status()`: Platte über `fs.statfsSync` (gemessen wird die Partition mit den Aufnahmen, Prozentwert wie `df` inklusive root-Reserve), RAM aus `/proc/meminfo` (`MemAvailable`, nicht `MemFree`), CPU-% aus der Differenz zweier `/proc/stat`-Messpunkte, Temperatur als wärmste SoC-/Core-/Center-/GPU-/NPU-Zone (WLAN-Zone `iwlwifi_1` ausgeschlossen). Eigener 2-s-Ticker (`unref()`), sonst wäre die erste CPU-Antwort nach jedem Start 0 %. - `GET /api/sysstat` in `server.js` — ohne `wrap()`/Warteschlange, weil es reine `/proc`-Lesevorgänge sind und die Abfrage keinen Prozess startet. - GUI (`web/src/main.js`): `sysmonMarkup()` zeichnet beide Chips, `paintSysmon()` schreibt die Werte ins DOM, `tickSysmon()` pollt alle 5 s (im Hintergrund-Tab ausgesetzt). Ein einziger Ticker für alle `render()`-Läufe; direkt nach `render()` werden die letzten Werte zurückgemalt, weil `render()` die Kopfzeile komplett neu schreibt und die Chips sonst bis zum nächsten Poll leer stünden. - Turbine: fünf Flügel als SVG-Pfade, Drehdauer per CSS-Variable `--spin`. Die Dauer läuft bewusst in **Stufen** (7 s Leerlauf … 0,35 s ab 90 %) — eine stufenlos nachgeführte `animation-duration` setzt die laufende Drehung bei jedem Poll neu an und ruckt sichtbar. `prefers-reduced-motion` stoppt sie. - Speicherring: `` (Umfang exakt 100) ⇒ `stroke-dasharray` = Prozentwert. - Ampel: grün < 75 %, gelb ab 75 %, rot ab 90 %; bei der Turbine zählt der schlimmste Wert aus CPU, RAM und Temperatur (45 °C → 0 %, 90 °C → 100 %). - Karten öffnen rein über CSS (`.sys-chip:hover .sys-pop`, zusätzlich `:focus-within` für Tastatur) — kein Handler, der einen `render()`-Durchlauf überleben müsste. **Dateien:** neu `app/sysstat.js`; geändert `app/server.js` (`.bak-sysmon`), `app/web/src/main.js` (`.bak-sysmon`), `app/web/src/style.css` (`.bak-sysmon`). Vite-Build + `systemctl restart callrec` (vorher geprüft: keine Analyse `pending`). Verifiziert: `/api/sysstat` liefert 52,4 % Platte (`df` sagt 53 %), 46 °C, 8 Kerne. Doku: Kapitel 07. ## 2026-08-17 — Kontext zum Gespräch: ein Freitextfeld nach dem Stop **Wunsch:** nach dem Beenden einer Aufnahme die Möglichkeit haben, Kontext zum gerade geführten Call/Meeting zu ergänzen — das verbessert die Analyse nach dem Transkript erheblich, weil das Modell Personen, Firmen, Anlass und Eigennamen nicht mehr aus fehlerbehaftetem ASR-Text erraten muss. **Entscheidung des Nutzers: EIN Freitextfeld, kein Formular.** Ein Vorschlag mit Teilnehmer-, Anlass-, Projekt- und Begriffsfeldern wurde ausdrücklich verworfen: die Box benutzen später auch technisch nicht versierte Menschen, und „Kontext hinzufügen“ ist eine Hürde weniger als vier Pflichtfelder. Geführt wird über den Platzhaltertext, die Struktur (Namen, Begriffe) zieht sich die Analyse selbst aus der Prosa. **Umsetzung:** - Neue Spalten `recordings.context_note` / `context_note_at` (`ensureSchema`), `db.setContextNote()` (Deckel 4 000 Zeichen, leerer Text löscht) und `db.getContextNote()`. - `PATCH /api/recordings/:id/context`. Die Antwort enthält `applies` und sagt damit **ehrlich**, ob die Notiz die laufende Verarbeitung noch erreicht: `auto` (wird noch transkribiert → fließt sicher ein), `running` (Analyse läuft schon, kann sie verpasst haben), `reanalyze` (fertig → nur ein neuer Lauf nutzt sie; die GUI fragt dann nach und startet `reanalyze-llm`). - `analyzeRecording()` liest die Notiz **erst kurz vor dem LLM-Aufruf** (`db.getContextNote`), nicht aus der Zeile vom Anfang: Sprecher-Erkennung und Transkription davor brauchen Minuten, und was der Nutzer in dieser Zeit tippt, soll noch mitzählen. - `llm.js`: neuer `contextBlock()` — die Notiz kommt in einem eigenen `"""`-Block mit der Auflage, sie **nur** für Schreibweisen, Zuordnung, Anlass und Gewichtung zu nutzen und daraus keine Aussagen, Ergebnisse oder Aufgaben abzuleiten. Eingespeist in alle Pfade: `analyzeSingle` (`PROMPT_CLEAN`, `PROMPT_CLEAN_LABELED`, `PROMPT_JSON`), `analyzeWhole` (`PROMPT_WHOLE_ANALYZE`) und `analyzeLong` (jeder Chunk-Prompt + Titel- und Überblick-Mikro-Call). Zwei Deckel: 1 200 Zeichen für die Prompts, die die Notiz einmal sehen, 600 für die Chunk-Prompts (dort landet sie in **jedem** Chunk von 3 200 Zeichen). Auf dem letzten von drei Chunk-Versuchen fällt sie mit dem `hint` weg. - GUI: Karte „📝 Kontext zum Gespräch“ über den Modulen der Detailansicht. Nach dem Stop steht sie offen und fokussiert (`state.ctxPromptFor`), sonst eingeklappt mit dem gespeicherten Text und „✏️ Bearbeiten“. Sie zeichnet sich **selbst** neu statt über `openRecording()` — ein Neuaufbau der Detailansicht setzt den `