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

13 KiB
Raw Permalink Blame History

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)

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:

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

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:

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)

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).