Files
pyramid/PROGRESS.md
T
Bernd Steckmeister f5b9ce9cd7 docs: M2-Modul-Schnitt definieren (docs/M2_MODULE_SCHNITT.md)
Öffentliche Schnittstellen für call_signaling, voice_channel, call_ui,
settings, verification, storage u.a. entworfen. Wichtigste Erkenntnis:
"Calls" ist architektonisch zwei getrennte Module (Matrix-VoIP vs.
LiveKit-SFU-Voice-Channels), keine gemeinsame Fassade. Korrigiert
nebenbei einen falschen Ist-Analyse-Befund (rooms_provider-Import war
fälschlich als toter Code eingestuft).
2026-07-03 10:27:35 +02:00

21 KiB
Raw Blame History

Pyramid Arbeitsprotokoll

Neueste Einträge OBEN. Jede Claude-Session schreibt hier nach jedem abgeschlossenen Schritt (und beim Abbruch mitten im Schritt den Zwischenstand). Format:

## JJJJ-MM-TT  Kurztitel
**Erledigt:** was fertig ist (mit Commit-Hash)
**Offen/Nächster Schritt:** der konkrete nächste Handgriff
**Stolperfallen:** was schiefging und wie es gelöst wurde (damit es nie zweimal passiert)
**Fragen an Bernd:** (optional) gesammelte Richtungsfragen, die nicht blockieren

2026-07-03 M2: Modul-Schnitt definiert (docs/M2_MODULE_SCHNITT.md)

Erledigt: Ausgehend von der Ist-Analyse (Eintrag darunter) die konkreten Modul-Grenzen + öffentliche Schnittstellen entworfen, dokumentiert in docs/M2_MODULE_SCHNITT.md. Reine Planung, kein Code geändert. Wichtigste Erkenntnis beim genaueren Lesen von voip_manager.dart und livekit_call_manager.dart: „Calls" ist architektonisch kein einzelnes Feature, sondern zwei unabhängige Mechanismen, die getrennt bleiben sollten:

  • PyramidVoipManager = natives Matrix-1:1-VoIP (m.call.*, direkte WebRTC-Peer-Connection, kein SFU)
  • LiveKitCallManager = SFU-basierte Voice-Channels (LiveKit-Raum, Presence via Matrix-State-Event, Screensharing, Mehrpersonen) das ist der Discord-artige Kanal aus M3/M4

Modul-Liste im Dokument: call_signaling, voice_channel, call_ui, settings (aufgesplittet in Sektionsdateien), verification (neu, aus Settings herausgelöst SAS/QR/Recovery-Key), storage (Ausbau der bestehenden settings_prefs.dart-Abstraktion zur Pflicht), auth (schon relativ sauber, kein akuter Bedarf), push (schon getrennt, eigener sqflite-Zugriff in background_push.dart ist bewusst so, kein Refactoring-Kandidat), chat_timeline/rooms (niedrigste Priorität, da strukturell am wenigsten verwoben Hauptproblem dort ist Dateigröße/Duplikate, nicht Kopplung). Jeweils mit Dart-Interface-Skizze bzw. Datei-Zielliste.

Empfohlene Umsetzungsreihenfolge: 1) Call-Pilot (call_signaling + voice_channel + call_ui, deckt gleich zwei ROADMAP-Punkte ab), 2) settings_modal.dart aufsplitten (geringes Risiko, reine Datei-Teilung), 3) verification herauslösen, 4) storage-Fassade schrittweise erzwingen, 5) chat_timeline/rooms zuletzt.

Offen/Nächster Schritt: Wartet auf Bernds Entscheidung, ob der Call-Pilot wie vorgeschlagen als Nächstes umgesetzt wird (siehe Frage unten). Erst danach den ROADMAP-Punkt „Call-Schicht entwirren" wirklich anfassen das ist die erste Stelle mit echtem Verhaltensrisiko in M2 (CLAUDE.md: nach Refactoring- Schritten immer App starten + Kernflows prüfen, hier zusätzlich brisant, weil GUI-Test auf dem Pi nicht möglich ist).

Stolperfallen: Der erste Ist-Analyse-Durchgang (Subagent) hatte einen falschen Befund (rooms_provider.dart-Import in app_state.dart sei toter Code) beim genaueren Lesen für den Modul-Schnitt widerlegt (voiceParticipantsProvider nutzt roomListProvider daraus). Bereits im Ist-Analyse-Eintrag korrigiert. Lehre: Subagent-Ist-Analysen sind ein guter Startpunkt, aber vor jedem darauf aufbauenden Schritt einzelne Befunde gegenlesen, nicht blind übernehmen.

Fragen an Bernd:

  • Call-Pilot wie in docs/M2_MODULE_SCHNITT.md vorgeschlagen (zuerst call_signaling/voice_channel/call_ui entwirren) als nächster Umsetzungsschritt in Ordnung, oder lieber zuerst settings_modal.dart aufsplitten (geringeres Risiko, aber deckt keinen ROADMAP-Punkt aus M2 direkt ab)? Ohne GUI-Zugriff auf dem Pi kann der riskantere Call-Umbau hier ohnehin nicht praktisch getestet werden das spricht eher für „auf dem PC anfangen", falls Zeitdruck besteht.

2026-07-03 M2: Ist-Analyse der Architektur geschrieben

Erledigt: M1 hat aktuell keinen auf dem Pi bearbeitbaren Punkt mehr offen (SQLCipher wartet auf Bernds Priorisierung, Härtetest braucht GUI/PC siehe Fragen an Bernd unten und Eintrag 2026-07-03 „3 schon vorhanden, 1 echte Lücke"). Deshalb mit M2 weitergemacht, erster Punkt: Ist-Analyse. Per Code-Lektüre (Zeilenzahlen, Import-Graph, grep) folgende Schwachstellen gefunden:

  • Gott-Dateien: widgets/settings_modal.dart mit 5541 Zeilen / 50+ Klassen bündelt mindestens 8 unabhängige Bereiche (Profil, Benachrichtigungen, Voice/TURN, E2EE-Verifizierung inkl. SAS/QR, Sessions, Account, Erscheinungsbild, Updates). Weitere große Dateien: message_group.dart (2780), rooms_panel.dart (2555), space_admin_dialog.dart (2278), chat_view.dart (2116), document_viewer.dart (1773), voice_channel.dart (1123), app_shell.dart (1073).
  • Call-Schicht unsauber verteilt statt hinter einer Schnittstelle: core/voip_manager.dart (Matrix-VoIP-Signalisierung), core/livekit_call_manager.dart (LiveKit-Transport), features/call/voice_channel.dart + mini_call_widget.dart (UI) aber chat_view.dart, matrix_client.dart, settings_modal.dart greifen alle DIREKT auf die Manager zu statt über eine gemeinsame Call-Fassade. Vier verschiedene Module importieren voice_channel.dart direkt. Genau das Gegenteil des „austauschbare Bausteine"-Ziels aus CLAUDE.md.
  • State-Management gemischt: 2 zentrale ChangeNotifier-Singletons (voip_manager.dart, livekit_call_manager.dart) statt Riverpod-Notifier, eingebunden über app_state.dart (callStateProvider, voipStateProvider). Migration sinnvoll, aber riskant wegen breiter Call-Sites und enger SDK-Callback-Kopplung (LiveKit-Room-Events, Matrix-WebRTCDelegate) sollte zusammen mit der Call-Fassade angegangen werden, nicht isoliert.
  • core/ ist nicht rein generisch: importiert Feature-Code direkt (app.dart/router.dartfeatures/auth/*, matrix_client.dart instanziiert PyramidVoipManager direkt). app_state.dart selbst ist ein Sammelbecken für Theme-, Call-, Voice-, Share-Target- und Spaces-Provider aus praktisch allen Features zentraler Kopplungspunkt, der jedem Modulschnitt im Weg steht. (Korrektur beim genaueren Hinsehen für den Modul-Schnitt-Punkt: der rooms_provider.dart-Import dort ist entgegen der ersten Vermutung KEIN toter Altlast-Import voiceParticipantsProvider nutzt roomListProvider daraus. Zeigt: Subagent-Befunde vor dem Weiterbauen selbst nachprüfen.)
  • Keine gemeinsame Storage-Schnittstelle: SharedPreferences.getInstance() wird an 12 Stellen direkt aufgerufen, obwohl core/settings_prefs.dart bereits eine StateNotifier-Abstraktion (BoolPref, StringSetPref) anbietet wird meist umgangen. sqflite wird zusätzlich unabhängig in background_push.dart nochmal geöffnet (eigene DB-Instanz fürs Background-Isolate). media_cache.dart ist dagegen sauber gekapselt gutes Vorbild für die anderen Speicherzugriffe.
  • Toter Code bestätigt: features/chat/message_bubble.dart (178 Zeilen, Klasse MessageBubble) wird nirgends instanziiert von message_group.dart abgelöst, kann beim Refactoring entfernt werden.
  • Duplikate: _formatTime/_formatDate unabhängig implementiert in pinned_messages_panel.dart:206, message_group.dart:322 UND :475 (zwei Mal im selben File!), sowie im toten message_bubble.dart:80/165.

Modul-Kandidaten für den nächsten Punkt („Modul-Schnitt definieren"):

  1. call_signaling (Matrix-VoIP) core/voip_manager.dart
  2. call_transport (LiveKit) core/livekit_call_manager.dart
  3. call_ui (nur UI, konsumiert 1+2 über Interface) features/call/*
  4. settings/* aufsplitten: profile, encryption_verification, sessions, notifications, appearance, account aus widgets/settings_modal.dart
  5. verification als eigenständiges Feature (SAS/QR/Recovery-Key), nicht Teil von Settings wiederverwendbar (z. B. künftig beim Login)
  6. storage gemeinsame Fassade, settings_prefs.dart als Pflichtzugang statt 12 Direktzugriffsstellen
  7. app_state.dart auflösen in feature-lokale Provider-Dateien
  8. chat_timeline message_group.dart + pinned_messages_panel.dart, dabei toten Code (message_bubble.dart) entfernen, _formatTime/_formatDate in chat/format_utils.dart zusammenführen

Offen/Nächster Schritt: M2, zweiter Punkt „Modul-Schnitt definieren": aus der obigen Kandidatenliste konkrete Modul-Interfaces (Dart-Abstract-Classes o. ä.) entwerfen, dann EIN Modul zuerst umbauen (Vorschlag: call_*, da am klarsten abgegrenzt und am dringendsten laut CLAUDE.md-Leitprinzip) als Pilot, bevor der Rest folgt. Kein Codeumbau in diesem Schritt, nur Schnittstellen-Design.

Stolperfallen: Keine reine Lesearbeit, kein Risiko für Bestandsdaten. Analyse wurde von einem Explore-Subagent erstellt und stichprobenartig plausibilisiert (Zeilenzahlen/Imports selbst nachgezählt), nicht jede Einzelbehauptung im Detail nachverifiziert.

Fragen an Bernd:

  • SQLCipher-Priorität weiterhin offen (siehe Eintrag „3 schon vorhanden, 1 echte Lücke" weiter unten) blockiert nicht, aber ohne Antwort bleibt der M1-Punkt bis zum nächsten PC-Praxistest liegen.
  • Für M2: Soll ich die Call-Schicht als erstes Pilot-Modul umbauen (mein Vorschlag oben), oder hast du eine andere Präferenz (z. B. erst die settings_modal.dart aufsplitten, weil die am unübersichtlichsten ist)?

2026-07-03 M1: Restliche Punkte geprüft 3 schon vorhanden, 1 echte Lücke gefunden

Erledigt:

  • Logout-Warnung bei fehlendem Key-Backup (Commit b5762ea): neue Funktion isKeyBackupMissing(client) in bootstrap_dialog.dart (prüft Cross-Signing + keyManager.isCached()). Beide Logout-Dialoge in settings_modal.dart zeigen jetzt eine rote Klartext-Warnung statt des neutralen Textes, wenn kein Backup eingerichtet ist.
  • Drei ROADMAP-Punkte waren bereits umgesetzt (nur nicht abgehakt) durch Code-Lektüre verifiziert, nicht neu gebaut:
    • Key-Backup einrichten/wiederherstellen: bootstrap_dialog.dart deckt beides über den Bootstrap-Zustandsautomaten des matrix-SDK ab (askNewSsss für Neueinrichtung, askUnlockSsss fragt auf einem neuen Gerät nach dem Recovery-Key). Wird automatisch getriggert, wenn Cross-Signing fehlt (app_shell.dart:_triggerBootstrap).
    • Geräte-/Sessionverwaltung: _SessionsSection in settings_modal.dart Liste, Umbenennen, SAS/QR-Geräteverifizierung, Einzel- und Massen-Abmeldung (mit Passwort-Reauth bei UIA-Anforderung).
  • Echte Sicherheitslücke gefunden: sqlcipher_flutter_libs ist eine Abhängigkeit in pubspec.yaml, wird aber nirgends im Code benutzt. matrix_client.dart (Haupt-App) und background_push.dart (Push-Hintergrund) öffnen pyramid.sqlite beide über den normalen, unverschlüsselten sqflite/sqflite_common_ffi-Factory. Die Krypto-DB (Access-Token, gepickelter Olm-Account, Megolm-Sessions, komplette Nachrichtenhistorie) liegt also im Klartext auf der Platte nur durch Androids App-Sandbox geschützt, nicht durch eine eigene Verschlüsselung.

Offen/Nächster Schritt: SQLCipher-Anbindung NICHT blind auf dem Pi umgesetzt siehe Stolperfalle unten. Braucht einen eigenen, vorsichtig geplanten PC-Termin: Backup der Test-DB, Migrationscode (bestehende Klartext-DB einmalig nach SQLCipher überführen), Test auf einem Gerät mit echten Daten, erst dann Rollout.

Fragen an Bernd:

  • Priorität der SQLCipher-Lücke: Soll das vor M2 (Refactoring) angegangen werden, oder reicht die App-Sandbox als Schutz erstmal aus (Angreifer bräuchte Root oder physischen Zugriff + ADB, um die Datei überhaupt zu lesen)? Die Migration bestehender Installationen (Uta!) ist der aufwändige/riskante Teil, nicht die Verschlüsselung selbst.

Stolperfallen:

  • Eine DB-Verschlüsselungs-Migration ist genau die Art Änderung, die laut CLAUDE.md nicht "mal eben" auf dem Pi ohne Testgerät gemacht werden darf: scheitert die Migration (falscher Schlüssel, falsche PRAGMA-Reihenfolge, Timing zwischen Haupt-App und Push-Hintergrund-Client), sind Utas Nachrichten und Krypto-Schlüssel unwiederbringlich weg. Deshalb hier nur dokumentiert, nicht implementiert.

2026-07-03 M1: Push-Regression vermutlich dieselbe Ursache wie Random-Logout

Erledigt: Vor dem Blick in den Push-Code die Vermutung geprüft, ob die Push-Entschlüsselungs-Regression und der Uta-Random-Logout-Bug (siehe Eintrag weiter unten) zusammenhängen Ergebnis: sehr wahrscheinlich ja, exakt dieselbe Ursache.

  • client.getEventByPushNotification(...) (aufgerufen in _bgDecryptAndShow, background_push.dart:84) ruft als ALLERERSTES ensureNotSoftLoggedOut() auf (matrix-Paket client.dart:1878). Diese Funktion prüft, ob der Access-Token in < 1 Minute abläuft, und löst dann _handleSoftLogout() aus — dieselbe Funktion, die ohne Refresh-Token (unser Zustand vor Commit 9dc83f7) sofort einen echten client.logout() auslöst (siehe Eintrag unten).
  • Der Hintergrund-Client aus _buildClient() liest dieselbe pyramid.sqlite wie der Haupt-Client. Ein logout()/clear() aus dem Hintergrund-Isolat würde also die komplette Session (inkl. Krypto-Schlüssel) für die GESAMTE App zerstören nicht nur für den einen Push. Das erklärt, warum das Problem als dauerhafte Regression erscheint ("seit dem letzten Update nie wieder"): Nach dem ersten Auto-Logout-Ereignis (ausgelöst beim Sync ODER beim Entschlüsseln eines Pushs) sind die Schlüssel weg, jede folgende Entschlüsselung schlägt fehl → BgEngine zeigt nur noch den „Neue Nachricht"-Platzhalter (Fallback nach 6 s), nie mehr den echten Text.
  • Der bereits committete Fix (refreshToken: true beim Login, 9dc83f7) behebt damit vermutlich BEIDE Symptome gleichzeitig: der Refresh-Token liegt in der gemeinsamen DB, also kann auch der Hintergrund-Client ihn lesen und einen drohenden Soft-Logout durch stillen Refresh abwenden, statt die Session zu zerstören.

Offen/Nächster Schritt: Gehört zum selben Praxistest wie der Random-Logout-Punkt oben zusätzlich beobachten, ob nach einer Weile (mehrere Stunden/Tage) echte Push-Inhalte statt Platzhalter ankommen. Schlägt das fehl, liegt eine zweite, unabhängige Ursache vor (dann docs/NOTIFICATIONS.md

  • Memory „Pyramid Push" erneut konsultieren, insbesondere die 3 dort gelisteten Fallstricke der Hintergrund-Entschlüsselung).

Stolperfallen: Diese Verbindung wurde rein durch Lesen des matrix-SDK- Quellcodes hergeleitet (~/.pub-cache/hosted/pub.dev/matrix-6.2.0/lib/src/client.dart), nicht durch Beobachtung auf einem echten Gerät bewusst als Vermutung markiert, bis der PC-Praxistest sie bestätigt.


2026-07-03 M1: Uta-Random-Logout Ursache gefunden + Fix (UNGETESTET/Pi)

Erledigt:

  • Alle Logout-Auslöser im Code kartiert:
    • 3 bewusste Nutzer-Logout-Buttons in settings_modal.dart (alle hinter Bestätigungsdialog) kein Kandidat für "random"
    • Der eigentliche Verdächtige: matrix-Paket (v6.2.0) Client._innerSync() behandelt M_UNKNOWN_TOKEN vom /sync-Endpunkt so: bei soft_logout: true_handleSoftLogout() → versucht refreshAccessToken()das SDK hatte nie einen Refresh-Token angefordert (client.login(...) in login_notifier.dart rief ohne refreshToken: true auf) → refreshAccessToken() wirft sofort "No refresh token available" → Catch-Block ruft await logout() auf: ein echter, destruktiver Logout, ausgelöst rein durch einen normalen Soft-Logout (z. B. abgelaufenes Access-Token), ganz ohne Nutzerinteraktion. Quelle: ~/.pub-cache/hosted/pub.dev/matrix-6.2.0/lib/src/client.dart:2374-2384 und :2496-2509.
  • Logging (Commit 63e4919): neue lib/core/auth_log.dart schreibt jeden Login-Status-Wechsel (client.onLoginStateChanged) sowie SDK-Warnungen/-Fehler (Logs().onLog) in auth_log.txt im App-Support-Verzeichnis übersteht App-Neustart. Zusätzlich loggen alle 3 Nutzer-Logout-Buttons sich selbst, damit sich künftig unterscheiden lässt: Nutzer-Logout vs. SDK-Auto-Logout. Reine Zusatzfunktion, keine Verhaltensänderung.
  • Fix (Commit 9dc83f7): client.login(...) fordert jetzt refreshToken: true an. Damit kann das SDK einen Soft-Logout künftig durch stillen Token-Refresh überstehen, statt die Sitzung zu zerstören. Unterstützt der Server keine Refresh-Tokens, bleibt das Verhalten exakt wie vorher (response.refreshToken ist dann null) der Fix kann also nichts verschlimmern.

Offen/Nächster Schritt:

  1. Praxistest auf dem PC (zwingend vor Haken in ROADMAP!): Login → Nachrichten senden/lesen → Logout → erneuter Login → alte verschlüsselte Nachrichten noch lesbar? Dabei prüfen, ob auth_log.txt (App-Support-Verzeichnis) die Login-Status-Wechsel sauber mitschreibt.
  2. Danach: den nächsten Punkt in M1 angehen (Push-Entschlüsselungs-Regression).
  3. Wichtig für Bernd/Uta: Der Fix wirkt nur für NEUE Logins. Utas bereits bestehende Sitzung bekommt erst nach einem einmaligen manuellen Neu-Login einen Refresh-Token und ist bis dahin weiterhin anfällig. Sobald der PC-Test grün ist, sollte Uta sich einmal aus- und wieder einloggen.

Stolperfallen:

  • Konnte auf dem Pi nicht mit echtem Login/Logout getestet werden (kein GUI, kein Matrix-Testaccount zur Hand) Fix beruht auf genauem Lesen des matrix-SDK-Quellcodes, nicht auf Beobachtung im Betrieb. Deshalb zwingend vor dem nächsten Haken in ROADMAP am PC nachprüfen.
  • chat_provider.dart:42 nutzt weiterhin das deprecated Timeline.onUpdate (Re-Entschlüsseln nach Schlüssel-Empfang) hängt mit demselben Sync-Bereich zusammen, aber bewusst nicht angefasst, um nicht zwei riskante Änderungen im selben Bereich gleichzeitig ungetestet zu committen.

2026-07-03 M0 abgeschlossen: Ordnung, flutter analyze sauber, README

Erledigt:

  • Liegen gebliebene Deprecation-Fixes aus einer abgebrochenen Session committet (withOpacitywithValues, activeColoractiveThumbColor, cacheExtentScrollCacheExtent, PublicRoomsChunkPublishedRoomsChunk) Commit 057ac1e
  • Restliche flutter analyze-Hinweise behoben Commit 401bd6a:
    • document_viewer.dart: Matrix4.translate/scaletranslateByDouble/ scaleByDouble (PDF-Zoom, gleiches Verhalten)
    • rooms_panel.dart: onReorderonReorderItem (Index-Korrektur jetzt intern, manuelles newIdx-- entfernt); implementation_imports-Hinweis kommentiert (SpaceChild wird vom matrix-Paket nicht öffentlich exportiert, kein Fix möglich)
    • voip_manager.dart: getSources()-Override kommentiert (Pflicht-Override der abstrakten MediaDevices-Klasse, die die Methode selbst deprecated hat)
    • settings_modal.dart: zwei BuildContext-nach-await-Stellen mit context.mounted abgesichert (Geräte umbenennen, Passwortabfrage bei Massenlogout)
    • Bewusst NICHT gefixt: chat_provider.dart:42 (Timeline.onUpdate deprecated, Ersatz wäre client.onSync + Raum-Filter). Hängt am E2EE-Redecrypt-Pfad (Re-Entschlüsseln nach Schlüssel-Empfang) laut CLAUDE.md heiliger Bereich, Umbau nur mit dediziertem Login→Nachrichten→Logout→Login-Test, nicht nebenbei.
  • lib/features/calls/ (Leiche) geprüft: existiert nicht mehr, nur lib/features/call/ mit den echten Dateien Punkt war bereits erledigt, nur nicht abgehakt.
  • README.md durch echte Projektbeschreibung ersetzt (Funktionsumfang, Build, Struktur) Commit 74d38fd
  • ROADMAP.md: M0 komplett abgehakt.

Offen/Nächster Schritt: M1, erster Punkt Uta-Random-Logout-Bug untersuchen (Logging an allen Logout-Pfaden einbauen, dann Ursache finden). Direkt danach die Push-Entschlüsselungs-Regression (docs/NOTIFICATIONS.md + Memory „Pyramid Push" vorher lesen).

Stolperfallen:

  • Kein test/-Ordner vorhanden → flutter test läuft ins Leere, keine automatisierte Absicherung. flutter analyze lief sauber (0 Fehler/Warnungen), aber die Lint-Fixes in document_viewer.dart (PDF-Zoom) und rooms_panel.dart (Drag-Reorder der Raumliste) sind UNGETESTET in echter UI nächster Windows-Lauf sollte kurz PDF-Zoom und Raum-Umsortieren per Drag prüfen.
  • Der ignore:-Kommentar für einen deprecated-Aufruf muss auf der Zeile unmittelbar über der betroffenen Codezeile stehen, nicht nur "in der Nähe" sonst wirkt er nicht (bei voip_manager.dart erst falsch über @override gesetzt, dann korrigiert).

2026-07-03 Arbeitsstruktur angelegt + Vollbackup (Setup, noch kein Code)

Erledigt:

  • Vollbackup inkl. Git-Historie: MatrixPi\backups\pyramid-kopie_backup_2026-07-03.tar.gz (510 MB)
  • CLAUDE.md (Arbeitsregeln), ROADMAP.md (M0M7 nach Bernds Prioritäten), dieses Protokoll
  • Klargestellt: C:\Users\nordm\pyramid - Kopie ist das EINZIGE aktuelle Repo; C:\Users\nordm\pyramid und MatrixPi\pyramid sind veraltete April-Stände (Commit c9af913)

Offen/Nächster Schritt: M0, Punkt 2 die ~140 uncommitteten Dateien mit git status sichten und in thematische Commits aufteilen (diese drei neuen Dateien mit committen), dann git push origin master.

Stolperfallen:

  • Letzter Commit ist vom 2026-04-28 (d706ace), gearbeitet wurde aber bis mindestens 2026-06-12 → zwischen Commits und Realität liegen 6 Wochen. Genau deshalb: ab jetzt kleine Commits nach jedem Schritt.
  • Git-Remote (Gitea auf dem Pi) enthält Zugangsdaten in der URL funktioniert, aber nur erreichbar, wenn der Pi läuft (unterwegs: WireGuard nötig).