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>
13 KiB
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.statfsSyncund ein gepufferter Größenscan überrecordings/(readdirrekursiv +stat, TTL 60 s, nur Audio-Endungen) — keindf/du/top-Prozess, damit die GUI im Sekundentakt fragen darf. - CPU-% aus der Differenz zweier
/proc/stat-Messpunkte.sysstat.jsmisst 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, nichtMemFree— 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()inweb/src/main.js, Poll alle 5 s, im Hintergrund-Tab (document.hidden) ausgesetzt. Ein einziger Ticker für allerender()-Durchläufe; nach jedemrender()werden die zuletzt geholten Werte sofort zurückgemalt, weilrender()die Kopfzeile komplett neu schreibt. - Drehzahl des Ventilators läuft in Stufen (
spinSeconds(): 7 s bei Leerlauf … 0,35 s ab 90 %). Eine stufenlos gesetzteanimation-durationwürde die laufende Drehung bei jedem Poll neu ansetzen — das ruckt sichtbar.prefers-reduced-motionstoppt 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-<thema>"]
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).