Files
Aime/docs/07-betrieb.md
SaschaandClaude Opus 5 acb840bd0a Web-GUI auf Port 80 (ohne Portnummer aufrufbar)
Die Unit startet Node als User=callrec (uid 1000) und konnte darum keinen
privilegierten Port binden - daher bisher :3000. Mit
AmbientCapabilities=CAP_NET_BIND_SERVICE bindet der unprivilegierte Prozess
jetzt Port 80 direkt, ohne Reverse-Proxy (nginx haette 20-40 MB RAM gekostet,
die neben dem residenten rkllm nicht frei sind). Kein Code-Eingriff noetig:
server.js liest process.env.PORT, das Frontend nutzt relative API-Pfade.
:3000 ist bewusst tot, alte Lesezeichen muessen angepasst werden.

Dazu: Doku-IPs auf aime.local/192.168.1.48 korrigiert (192.168.1.200 war
veraltet) und die Geisteranzeige-Diagnose vom 26.08. in Kapitel 03 + Changelog
festgehalten (verwaister Browser-Reiter zaehlt ewig weiter, fmt() ohne Stunden).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 08:53:14 +02:00

227 lines
13 KiB
Markdown
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.
# 07 — Betrieb
> ⚠️ Dieses Repo ist **privat**; hier stehen LAN-Zugangsdaten. Nicht öffentlich stellen.
## Zugänge
| System | Zugang |
|---|---|
| Board SSH | `root@aime.local` — aktuell `192.168.1.48` (WLAN, DHCP) / `192.168.1.50` (LAN, statisch, nur bei gestecktem Kabel), PW `123456` |
| Web-GUI | `http://aime.local` bzw. `http://192.168.1.48` — **Port 80 seit 2026-08-26**, keine Portnummer mehr nötig |
| MariaDB (auf dem Board) | DB `callrec`, User `callrec`, PW `callrec123` (kein `sqlite3`/CLI-Client — `mysql` nutzen) |
| Gitea (diese Doku) | `http://192.168.1.199:3000`, User `sascha` |
| App-User auf dem Board | `callrec`, Code in `/opt/callrec` |
## systemd-Einheiten
| Einheit | Scope | Aufgabe |
|---|---|---|
| `callrec.service` | system | Node-App (Web-GUI **:80**, Aufnahme, Pipeline) — bindet den privilegierten Port als `User=callrec` über `AmbientCapabilities=CAP_NET_BIND_SERVICE` |
| `callrec-btwatch.service` | system | BT-Headset-Auto-Connect (card-aware) |
| `callrec-perf.service` | system | CPU-Governor `performance` + A76-Pinning |
| `aime-usb-audio.service` | user | PipeWire-Topologie beim Boot (callmix, Loopbacks, Remap) |
| `aime-micgate.service` | user | LADSPA-Noise-Gate-Filter-Chain |
| `aime-volume-headset.service` | user | Windows-Volume → Headset-Sink (UAC2 numid=7) |
| `aime-headset-keys.service` | root | H390-Inline-Tasten → Lautstärke |
| `aime-iphone-route.service` | — | HFP-Routing-Watcher für iPhone-Anrufe. **Achtung:** die Unit liegt derzeit nicht unter `/etc/systemd/system/`, nur das Skript `/opt/callrec/aime-iphone-route.sh` ist da (Stand 2026-08-03, war schon vor dem eMMC-Umzug so) |
| `aime-bootsync.service` | system | spiegelt `/boot` der eMMC auf die SD-Karte (Start + Shutdown), siehe Speicher-Abschnitt |
| `filter-chain.service` (PipeWire-Standard) | user | **MASKIERT** — würde micgate doppeln |
| `aime-audio-out` / `aime-volume` | user | ES8323-Ära, **disabled** |
## Speicher: System auf der eMMC, Bootdateien auf der SD (seit 2026-08-03)
```mermaid
flowchart LR
ROM["BootROM RK3588S"] --> IDB["idbloader + U-Boot<br/>(eMMC, Sektor 64 / 16384)"]
IDB --> SCR{"boot.scr + armbianEnv.txt<br/>zuerst gefunden auf:"}
SCR -->|SD-Karte| KRN["Kernel + initrd<br/>von der SD"]
KRN -->|"rootdev=UUID=3ba18bc1…"| ROOT["/ = eMMC<br/>mmcblk0p1 „aime-emmc“"]
ROOT -.->|"aime-bootsync spiegelt /boot"| KRN
```
| Gerät | Größe | Rolle |
|---|---|---|
| `mmcblk0p1` (eMMC, `aime-emmc`) | 58 GB, 28 GB belegt | **`/` — System, Datenbank, Aufnahmen, Modelle** |
| `mmcblk1p1` (SD, `armbi_root`) | 118 GB | `boot.scr`/Kernel/initrd **und** eine vollständige Kopie des alten Systems als Rückfallebene. Nicht eingehängt — die SD wird im Betrieb weder gelesen noch beschrieben. |
**Warum die eMMC:** sie ist für Dauerbetrieb gebaut (Verschleiß auslesbar: `life_time 0x01`,
`pre_eol_info 0x01` = unter 10 % verbraucht), die SD-Karte ist der übliche Ausfallpunkt und stirbt
meist ohne Vorwarnung. Aime schreibt viel — jede Aufnahme WAV + MP3, dazu Datenbank und Suchindex.
**Der Haken, den man kennen muss:** U-Boot liest `boot.scr` und `armbianEnv.txt` von der **SD-Karte**,
solange sie steckt — nachgewiesen durch einen Neustart, bei dem eine auf die SD-Root zeigende
`armbianEnv.txt` das alte System hochfuhr. Der Kernel kommt also von der SD, das Root von der eMMC.
Ohne Gegenmaßnahme würde ein Kernel-Update auseinanderlaufen: neue Module auf der eMMC, alter Kernel
von der SD.
**`aime-bootsync.service`** löst das — spiegelt `/boot` beim Start **und** beim Herunterfahren auf die
SD (`rsync -a --delete --exclude='*.bak*'`), sodass beide Medien denselben Kernel tragen. Stirbt die
SD-Karte, findet U-Boot dort nichts mehr und nimmt die eMMC-Bootkette, die vollständig vorhanden ist.
Beide `armbianEnv.txt` (eMMC und SD) tragen `rootdev=UUID=3ba18bc1-bb8e-4a34-9c61-18a27f2c7adf`.
Auf der SD liegt daneben `armbianEnv.txt.bak-vor-emmc-umzug` mit der alten SD-UUID
`ab30e77b-…` — wer das alte System wieder booten will, kopiert die Datei zurück.
**Wichtig bei einem neuen Dateisystem:** `mkfs.ext4` aus e2fsprogs 1.47 setzt `orphan_file` und
`metadata_csum_seed`. `orphan_present` ist ein Incompat-Merkmal — ein Treiber, der es nicht kennt
(U-Boots ext4-Leser), verweigert das Lesen. Vor dem ersten Start entfernen:
```bash
tune2fs -O ^orphan_file,^metadata_csum_seed /dev/mmcblk0p1 && e2fsck -fy /dev/mmcblk0p1
```
## Wichtige Pfade auf dem Board
| Pfad | Inhalt |
|---|---|
| `/opt/callrec/*.js` | server / recorder / llm / diar / spk / db / bt / sysstat |
| `/opt/callrec/web/` | Vite-Frontend (`npx vite build` nach Änderungen an `src/`) |
| `/opt/callrec/aime-*.sh` | Setup-/Bridge-/Cap-Skripte |
| `/opt/callrec/milestones/m1-20260709/` | Meilenstein-1-Snapshot inkl. MANIFEST.md (Rollback) |
| `/opt/callrec/*.bak-*` | Datei-Backups je Änderung (z. B. `.bak-structsum`, `.bak-anchor`) |
| `/etc/sudoers.d/callrec-cpucap` | NOPASSWD für `aime-cpucap.sh` (Thermal-Cap) |
## Systemanzeige in der Kopfzeile (seit 2026-08-17)
Zwischen dem Tab „Bluetooth“ und dem Hell/Dunkel-Schalter stehen zwei Symbole, die den Zustand des
Boards ohne SSH zeigen:
| Symbol | Zeigt an | Klappkarte bei Mauskontakt |
|---|---|---|
| Ring + Prozentzahl | Anteil des **Aufnahme-Vorrats**, den die Aufnahmen schon belegen | Aufnahmen (GB + Dateizahl) mit Balken, Noch frei, Gesamt für Aufnahmen |
| Ventilator | CPU-Last — je höher, desto schneller die Drehung | CPU % (+ Kernzahl), RAM % mit GB, CPU-Temperatur |
**Bezugsgröße der Prozentzahl** (Wunsch des Nutzers, 2026-08-17): nicht die Partition, sondern der Platz,
den ein *frisch aufgesetztes System dieser Ausbaustufe* für Aufnahmen übrig hätte:
```
100 % = freier Platz + Summe aller Audiodateien unter recordings/
belegt = Summe aller Audiodateien unter recordings/
```
System, Modelle und `node_modules` zählen also weder oben noch unten mit — die Anzeige beantwortet
„wie viel von meinem Aufnahme-Vorrat ist weg?“ und nicht „wie voll ist die eMMC?“. Die Klappkarte zeigt
bewusst nur die drei Zeilen Aufnahmen / Noch frei / Gesamt für Aufnahmen; den `df`-Wert (`partPct`) und
den Erklärtext zur 100-%-Bezugsgröße hat der Nutzer wieder streichen lassen. `/api/sysstat` liefert
`partPct` und `totalBytes` weiterhin.
**Mitgezählt wird jede Audiodatei unter `recordings/`, nicht nur die MP3s** — zu jeder Aufnahme liegt
dort auch die WAV-Rohdatei, und die ist rund viermal so groß wie das MP3 (Stand 2026-08-17: 52 MP3 =
0,39 GB, 51 WAV = 1,57 GB). Ebenso zählen die Aufnahmen im **Papierkorb** mit, denn deren Dateien
liegen bis zum Ablauf der 30 Tage weiter auf der Platte (Stand 2026-08-17: 16 aktiv, 36 im Papierkorb).
**Zeilenumbruch:** `.sys-row` steht auf `white-space: nowrap`, die Karte wächst stattdessen in die
Breite (`.sys-pop-disk { min-width: 285px }`). Vorher rutschte bei „Gesamt für Aufnahmen 27,5 GB“ die
Einheit allein in die nächste Zeile und sah aus wie ein fehlender Wert.
- Datenquelle: `GET /api/sysstat` (`app/sysstat.js`). `/proc`, `fs.statfsSync` und ein **gepufferter**
Größenscan über `recordings/` (`readdir` rekursiv + `stat`, TTL 60 s, nur Audio-Endungen) — **kein**
`df`/`du`/`top`-Prozess, damit die GUI im Sekundentakt fragen darf.
- CPU-% aus der Differenz zweier `/proc/stat`-Messpunkte. `sysstat.js` misst dafür **selbst** alle 2 s
(`setInterval(...).unref()`); ohne diesen eigenen Ticker wäre die erste Antwort nach jedem Start 0 %.
- RAM rechnet mit `MemAvailable`, nicht `MemFree` — sonst stünde das Board wegen Cache dauernd bei 95 %.
- Temperatur = wärmste der **CPU-Zonen** (`soc-thermal`, `bigcore0/1-thermal`, `littlecore-thermal`).
GPU-, NPU-, Center- und WLAN-Zone (`iwlwifi_1`) bleiben draußen: die Karte beschriftet den Wert als
„CPU-Temperatur“, also darf auch nur CPU darin stecken. Die Zone selbst wird nicht mehr angezeigt.
- Frontend: `sysmonMarkup()` / `paintSysmon()` / `tickSysmon()` in `web/src/main.js`, Poll alle 5 s, im
Hintergrund-Tab (`document.hidden`) ausgesetzt. Ein einziger Ticker für alle `render()`-Durchläufe;
nach jedem `render()` werden die zuletzt geholten Werte sofort zurückgemalt, weil `render()` die
Kopfzeile komplett neu schreibt.
- Drehzahl des Ventilators läuft in **Stufen** (`spinSeconds()`: 7 s bei Leerlauf … 0,35 s ab 90 %). Eine
stufenlos gesetzte `animation-duration` würde die laufende Drehung bei jedem Poll neu ansetzen — das
ruckt sichtbar. `prefers-reduced-motion` stoppt die Animation ganz.
- Icon: vier gebogene Flügel (Pinwheel) im **feststehenden** Gehäusering — nur die Flügelgruppe dreht
sich, sonst wäre an einem runden Ring keine Drehung zu sehen.
- Farbe: grün < 75 %, gelb ab 75 %, rot ab 90 %; beim Ventilator zählt der schlimmste Wert aus CPU, RAM
und Temperatur (Temperatur skaliert 45 °C → 0 %, 90 °C → 100 %).
### Ablauf einer Abfrage
```mermaid
sequenceDiagram
autonumber
participant GUI as Browser (main.js)
participant API as server.js
participant SYS as sysstat.js
participant K as Kernel (/proc, statfs)
participant FS as recordings/
Note over SYS: eigener Ticker alle 2 s:<br/>/proc/stat lesen, CPU-% aus der Differenz
loop alle 5 s (im Hintergrund-Tab ausgesetzt)
GUI->>API: GET /api/sysstat
API->>SYS: status()
SYS->>K: statfs(recordings/) · /proc/meminfo · thermal_zone*/temp
K-->>SYS: frei/gesamt · MemAvailable · °C je CPU-Zone
alt Scan älter als 60 s
SYS->>FS: readdir rekursiv + stat (nur Audio-Endungen)
FS-->>SYS: Bytes + Dateizahl → Puffer
else Puffer frisch
Note over SYS: Puffer wiederverwenden,<br/>Scan höchstens 1× pro Minute
end
SYS-->>API: {cpu, ram, disk, temp}
API-->>GUI: JSON
GUI->>GUI: paintSysmon(): Ring, Balken, Zahlen,<br/>--spin (Stufe), Ampelfarbe
end
Note over GUI: nach jedem render() sofort<br/>paintSysmon(sysmon.last) — die Kopfzeile<br/>wird komplett neu geschrieben
```
Rechenwege der beiden Anzeigen:
```mermaid
flowchart LR
subgraph Ring["Ring — Aufnahme-Vorrat"]
F["freier Platz<br/>statfs.bavail"] --> B(("Vorrat<br/>= frei + Aufnahmen"))
R["Aufnahmen<br/>Summe aller mp3 + wav in recordings/<br/>(inkl. Papierkorb)"] --> B
R --> P["Prozent = Aufnahmen / Vorrat"]
B --> P
end
subgraph Fan["Ventilator — Last"]
C["CPU %<br/>/proc/stat-Differenz"] --> S["spinSeconds()<br/>7 s … 0,35 s in Stufen"]
C --> W{"schlimmster Wert"}
M["RAM %<br/>MemAvailable"] --> W
T["CPU-Temperatur<br/>soc/bigcore/littlecore"] --> W
W --> A["Ampel grün / gelb / rot"]
end
```
## Deploy-Ablauf (von Windows aus)
```mermaid
flowchart LR
DEV["Datei lokal ändern<br/>(pscp vom Board holen)"] --> BAK["Backup auf Board:<br/>cp X X.bak-&lt;thema&gt;"]
BAK --> UP["pscp hochladen"]
UP --> FE{"Frontend<br/>geändert?"}
FE -- ja --> BUILD["ssh: cd /opt/callrec/web && npx vite build"]
FE -- nein --> RESTART
BUILD --> RESTART["systemctl restart callrec"]
RESTART --> VERIFY["Verifizieren: journalctl -u callrec,<br/>GUI-Test, ggf. reanalyze-llm"]
VERIFY --> DOC["Doku + Changelog in Gitea aktualisieren"]
```
- Windows-Seite: PuTTY-Tools (`plink -batch -pw …`, `pscp`) — OpenSSH-Keys sind nicht hinterlegt.
- Node-Syntax vor Upload prüfen: `node --check datei.js`.
## Troubleshooting
| Symptom | Ursache / Fix |
|---|---|
| Zeile hängt auf `transcribing` | Absturz mitten in Diarization → GUI „Neu transkribieren“ (`/retranscribe`) |
| Summary wirkt leer nach Retranskription | GUI muss `analysis_status` pollen, nicht `status` — Analyse ist fire-and-forget |
| Board plötzlich aus | Thermik prüfen — Schutzabschaltung; Cap-Skript/Sudoers intakt? (Kap. 01) |
| Zu viele Sprecher | Audio geclippt? (Gain-Staging Kap. 02) Sonst „Analyse neu“ mit gepinnter Zahl |
| Teilnehmer ≠ Sprecher | Teilnehmer kommen aus Diarization; Namen im Sprecher-Editor vergeben |
| Kein far-end bei iPhone-Aufnahme | BT-Gerät `Trusted=yes`? HFP-Nodes erscheinen nur IM Anruf (Kap. 06) |
| GUI zeigt Quelle ohne Signal | `.monitor`-Falle — Quelle mit `parecord` + `ffmpeg astats` prüfen (Kap. 02) |
| `pactl load-module module-filter-chain` → „No such entity“ | Filter-Chain ist Core-Modul → Drop-in-Config nutzen (Kap. 02) |
| rkllm/Whisper `rc=-6` | RAM-Contention — nicht beide NPU-resident; NPU-Lock beachten (Kap. 05) |
| Analyse „KI Fehler“ bei langen Aufnahmen | Kontext-Überlauf → Chunk-Pfad greift; bei schwachem Ergebnis „Analyse neu“ |
## Offene Punkte (Stand 2026-07-16)
- Bedingter Takt-Cap (nur > 15 min Aufnahmedauer) — angeboten, nicht gebaut (~1,4× schnellere Kurzclips).
- Feinabstimmung Mikro/PC-Balance in einem echten Headset-Call.
- Glossar Phase 2: nächtlicher systemd-Timer + large-v3-Kandidaten-Vorschläge mit Bestätigung (verschoben).
- iPhone-Pfad: automatischer Aufnahme-Start + voller E2E-Test (Kap. 06).
- Qwen-1.5B als schnellerer LLM-Kandidat für Kurzclips (verschoben).
- Aufnahme #41 bleibt dauerhaft geclippt (Referenz für den Gain-Bug).