Sync-Ausbau: Löschpfad gefixt — Spec + Plan entblockt

Hermes hat den irreversiblen Löschpfad in melo_cloud.py behoben: Der
Dedup-Zweig von upload() stellt die Datei jetzt aus den hochgeladenen
Bytes wieder her, wenn registry_pfad(sid) leer ist, statt sie zu
verwerfen.

End-to-end verifiziert (eigene Testdatei, Fake-Nutzer, Testdaten danach
restlos entfernt — Registry vor und nach dem Test bei 325 Titeln):
Upload → beide Kopien da · Löschung → beide weg, Download 404 (Bug
reproduziert) · erneuter Upload → beide Kopien zurück, Download
bitgenau identisch mit dem Original.

Damit ist die letzte offene Frage der Spec (A6) beantwortet und keine
Stufe mehr blockiert. Tasks 5-7 des Plans sind entblockt; Tasks 10-12
hängen nur noch an der Produktfrage "wird Feature 2 überhaupt gebaut?"
(0 Playlisten am Server).

Nebenbefund aus dem Test, als Backlog-Notiz festgehalten: _link_user()
legt im Dedup-Zweig einen dritten Hardlink unter users/<user>/ an, den
_entferne_datei_wenn_verwaist() nicht abräumt. Server-Hygiene für
Hermes, kein Datenverlust.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcDiyJdVRqh1TtJk5JiabX
This commit is contained in:
Hermes (Server)
2026-08-27 10:31:49 +02:00
co-authored by Claude Sonnet 5
parent 2a1cf9cdc2
commit 1940e2b790
2 changed files with 116 additions and 68 deletions
@@ -94,10 +94,11 @@ Tests, SharedPreferences, Provider. **Keine neue Dependency.**
- Ponytail-Prinzip: minimale, robuste Lösungen. Drei Nutzer, ~325 Songs, - Ponytail-Prinzip: minimale, robuste Lösungen. Drei Nutzer, ~325 Songs,
0 Favoriten und 0 Playlisten auf dem Server — nichts überbauen. 0 Favoriten und 0 Playlisten auf dem Server — nichts überbauen.
- **Reihenfolge ist bindend.** Tasks 14 (Favoriten-Fix) hängen an nichts und - **Reihenfolge ist bindend.** Tasks 14 (Favoriten-Fix) hängen an nichts und
kommen zuerst. Tasks 57 sind **blockiert bis Dustin A6 beantwortet hat** kommen zuerst. **Tasks 57 sind seit dem 2026-08-27 ENTBLOCKT** — der
(irreversibler Löschpfad). Tasks 1012 sind **blockiert bis der Server-Fix Löschpfad ist serverseitig behoben und end-to-end verifiziert (siehe Spec,
`melo_cloud.py:505` steht** bzw. bis Dustin entscheidet, ob Feature 2 §ERLEDIGT). Tasks 1012 bleiben blockiert, aber nur noch durch die
überhaupt gebaut wird. Produktfrage „wird Feature 2 überhaupt gebaut?" (0 Playlisten am Server) —
Details bei Task 10.
--- ---
@@ -1207,13 +1208,13 @@ git commit -m "Herz-Tipp meldet den Favoriten sofort deterministisch an die Clou
### Task 5: `ladeAusgewaehlteHoch` + `abbrechen()` im `SyncService` ### Task 5: `ladeAusgewaehlteHoch` + `abbrechen()` im `SyncService`
> **BLOCKIERT bis Dustin A6 beantwortet hat.** > ✅ **ENTBLOCKT seit 2026-08-27.** Die frühere Blockade lautete: dieser Task
> A6 = „Ist der irreversible Löschpfad gewollt?" (Spec, §OFFENE ENTSCHEIDUNG). > gibt mehr Titeln eine cloudId und vergrößert damit die Angriffsfläche des
> Dieser Task gibt mehr Titeln eine cloudId und vergrößert damit genau die > irreversiblen Löschpfads. Dustins Antwort auf A6 war „nein, nicht gewollt";
> Angriffsfläche des Löschpfads. Lautet die Antwort **„nein"**, gehört der > Hermes hat den Fix am selben Tag eingebaut (Datei-Wiederherstellung im
> Server-Auftrag „Datei-Wiederherstellung im Dedup-Zweig von `upload()`" > Dedup-Zweig von `upload()`), end-to-end verifiziert. Ein versehentlicher
> **davor**. Lautet sie **„ja"**, darf sofort begonnen werden. > Löschvorgang ist jetzt durch erneutes Hochladen reparabel.
> **Nicht ohne Antwort anfangen.** > **Darf gebaut werden.**
**Dateien:** **Dateien:**
- Ändern: `lib/services/sync_service.dart:143-155` (Feld `_abbruchGewuenscht`) - Ändern: `lib/services/sync_service.dart:143-155` (Feld `_abbruchGewuenscht`)
@@ -1555,8 +1556,9 @@ git commit -m "Gezielter Upload mit Abbrechen aus dem Auswahl-Modus"
### Task 6: Auswahl-Modus-Aktion „Auf den Server laden" ### Task 6: Auswahl-Modus-Aktion „Auf den Server laden"
> **BLOCKIERT bis Dustin A6 beantwortet hat** — dieselbe Begründung wie > ✅ **ENTBLOCKT seit 2026-08-27** — dieselbe Begründung wie Task 5 (die
> Task 5: die Aktion ist die Oberfläche zu `ladeAusgewaehlteHoch`. > Aktion ist die Oberfläche zu `ladeAusgewaehlteHoch`), und dieselbe
> Auflösung: Löschpfad serverseitig behoben. **Darf gebaut werden.**
**Dateien:** **Dateien:**
- Ändern: `lib/shared/auswahl_leiste.dart:18-66` (`AuswahlLeiste` bekommt - Ändern: `lib/shared/auswahl_leiste.dart:18-66` (`AuswahlLeiste` bekommt
@@ -1875,14 +1877,11 @@ git commit -m "Auswahl-Modus: Auf den Server laden (nur in Meine Musik)"
### Task 7: Einzel-Song-Offline ### Task 7: Einzel-Song-Offline
> **BLOCKIERT bis Dustin A6 beantwortet hat** — nur, weil die Spec Stufe 2 > ✅ **ENTBLOCKT seit 2026-08-27** (Löschpfad serverseitig behoben). Der Task
> (Feature 3 **und** 4) als Ganzes hinter die A6-Antwort stellt. > war ohnehin nur mitblockiert, weil die Spec Stufe 2 als Ganzes hinter die
> **Ehrlicher Hinweis für Dustin:** Die inhaltliche Begründung der Spec > A6-Antwort stellte — die inhaltliche Begründung traf auf Feature 4 nie zu:
> („vergrößert die Angriffsfläche des Löschpfads, weil mehr Titel eine cloudId > ein Download vergibt keine cloudId und meldet nichts zum Server.
> bekommen") trifft auf **Feature 4 nicht zu** — ein Download vergibt keine > **Darf gebaut werden.**
> cloudId und meldet nichts zum Server. Dieser Task könnte also gefahrlos
> vorgezogen werden. Das zu entscheiden ist Dustins Sache, nicht die des
> Umsetzenden — bis dahin bleibt der Task blockiert.
**Dateien:** **Dateien:**
- Ändern: `lib/services/download_service.dart:70-111` (neue Methode - Ändern: `lib/services/download_service.dart:70-111` (neue Methode
@@ -3989,11 +3988,15 @@ git push -u origin feature/sync-ausbau
## Offene Punkte, die vor bzw. während der Umsetzung an Dustin gehen ## Offene Punkte, die vor bzw. während der Umsetzung an Dustin gehen
1. **A6 — irreversibler Löschpfad** (blockiert Tasks 57). 1. ~~**A6 — irreversibler Löschpfad**~~ ✅ **ERLEDIGT 2026-08-27.** Dustins
„Verschwindet eine lokale Datei, löscht der Abgleich sie am Server und aus Antwort: nicht gewollt. Hermes hat die Datei-Wiederherstellung im
der Navidrome-Bibliothek; erneutes Hochladen repariert das nicht. Gewollt?" Dedup-Zweig von `upload()` eingebaut, end-to-end verifiziert (Upload →
*Nebenfrage:* Task 7 (Einzel-Song-Offline) vergibt keine cloudId und ist Löschung → erneuter Upload stellt beide Kopien bitgenau wieder her).
von der Begründung sachlich nicht betroffen — darf er vorgezogen werden? Tasks 57 sind damit entblockt.
*Offen geblieben, aber kein Blocker:* Die App tombstoned weiterhin
großzügig (nicht eingehängte SD-Karte genügt) und die Lösch-Bremse greift
erst ab 109 von 325 Titeln — der Schaden ist jetzt nur reparabel, nicht
verhindert. Ob die App vorsichtiger werden soll, ist eine eigene Frage.
2. **Feature 2 (Playlist-Sicherung, Tasks 1012).** Bauen oder streichen? 2. **Feature 2 (Playlist-Sicherung, Tasks 1012).** Bauen oder streichen?
Heute 0 Playlisten am Server. Empfehlung: vertagen. Heute 0 Playlisten am Server. Empfehlung: vertagen.
Falls bauen: hängt der Server-Fix `melo_cloud.py:505` wirklich davor, Falls bauen: hängt der Server-Fix `melo_cloud.py:505` wirklich davor,
@@ -1,11 +1,13 @@
# Sync-Ausbau: Favoriten-Fix, Auswahl-Upload, Einzel-Song-Offline, Playlist-Sicherung # Sync-Ausbau: Favoriten-Fix, Auswahl-Upload, Einzel-Song-Offline, Playlist-Sicherung
Status: Nachgeschärft nach agent-review-panel (Urteil Phase 14, 2026-08-27: Status: **Freigegeben, umsetzungsbereit** (2026-08-27).
Score 6/10, Verdikt „Spec nachschärfen dann freigeben"). Alle 17 Nachgeschärft nach agent-review-panel (Urteil Phase 14: Score 6/10,
Aktionspunkte A1A17 sind eingearbeitet. Bedingung des Richters: diese Verdikt „Spec nachschärfen dann freigeben"), alle 17 Aktionspunkte
Fassung wird **einmal kurz gegengelesen**, bevor der Implementierungsplan A1A17 eingearbeitet, Gegenlesen durch einen frischen Prüfer bestanden
entsteht — A1, A5 und A7 ändern, *was* gebaut wird, nicht nur wie es (Bedingung des Richters erfüllt). Die einzige offene Frage (A6,
beschrieben ist. schpfad) ist beantwortet und serverseitig behoben — siehe §ERLEDIGT.
**Keine Stufe ist blockiert.** Implementierungsplan:
`docs/superpowers/plans/2026-08-27-sync-ausbau.md` (13 Tasks).
## Kontext ## Kontext
@@ -56,25 +58,41 @@ der Bau-Ansatz bleiben unangetastet):
- Die **Playlist-Konfliktregel** („längerer Stand gewinnt") entfällt - Die **Playlist-Konfliktregel** („längerer Stand gewinnt") entfällt
ersatzlos, weil es ohne Rück-Merge keinen Konflikt mehr gibt (A5). ersatzlos, weil es ohne Rück-Merge keinen Konflikt mehr gibt (A5).
## OFFENE ENTSCHEIDUNG — Rückfrage an Dustin (noch nicht beantwortet) ## ERLEDIGT — Löschpfad ist gefixt (2026-08-27)
> **Ist der irreversible Löschpfad gewollt?** > **Die Rückfrage an Dustin („Ist der irreversible Löschpfad gewollt?")
> Verschwindet eine lokale Datei (SD-Karte nicht eingehängt, Berechtigung > ist beantwortet: nein.** Hermes hat den Fix am selben Tag in
> entzogen, Dateimanager), löscht der Sync sie auf dem Server — und damit > `melo_cloud.py` eingebaut und den Dienst neu gestartet.
> auch aus der Navidrome-Bibliothek. Erneutes Hochladen repariert das
> nachweislich **nicht**. Die technische Kette steht vollständig belegt
> unter §Risiken → „Irreversibler Löschpfad".
> >
> - **Antwort „ja, gewollt“** → es ist eine dokumentierte Eigenschaft, > **Was sich geändert hat:** Im Dedup-Zweig von `upload()` prüft der
> nichts weiter zu tun. > Server jetzt `registry_pfad(sid) is None` und stellt in diesem Fall die
> - **Antwort „nein“** → ein Server-Auftrag (Dedup-Zweig stellt die Datei > Datei aus den hochgeladenen Bytes wieder her (`shutil.move` nach `REG`
> wieder her, wenn `registry_pfad(sid)` leer ist) gehört **vor** > plus `UPDATE registry SET path`), statt sie über `os.remove(tmp)` zu
> Feature 3. > verwerfen. Der bestehende `verknuepfe_navidrome`-Aufruf hängt sie
> danach automatisch zurück in die Navidrome-Bibliothek.
> >
> Kein Reviewer kann das entscheiden — es ist eine Produktfrage. > **End-to-End verifiziert** (2026-08-27, eigene Testdatei, Fake-Nutzer,
> **Stand: unbeantwortet.** Die Reihenfolge in §Reihenfolge ist bewusst > Testdaten danach restlos entfernt — Registry vor und nach dem Test bei
> so gewählt, dass mit Stufe 1 begonnen werden kann, ohne dass die > 325 Titeln): Upload → beide Kopien da, Download bitgenau · Löschung →
> Antwort vorliegt. > beide Dateien weg, Download 404 (Bug reproduziert) · **erneuter Upload
> → beide Dateien zurück, Download wieder bitgenau identisch.**
>
> **Folge für diese Spec: keine Stufe ist mehr blockiert.** Der
> ursprüngliche Grund, Feature 3 (Auswahl-Upload) hinter die Klärung zu
> stellen — es gibt mehr Titeln eine cloudId und vergrößert damit die
> Angriffsfläche —, ist entfallen: Ein versehentlicher Löschvorgang ist
> jetzt durch erneutes Hochladen reparabel. Die Reihenfolge in
> §Reihenfolge bleibt trotzdem so bestehen, weil sie auch aus anderen
> Gründen sinnvoll ist (Stufe 1 behebt den einzigen belegten Bug und
> hängt an nichts).
>
> **Nebenbefund aus dem Test, nicht blockierend:** `_link_user()` wird
> nur im Dedup-Zweig aufgerufen (der Neu-Zweig kehrt vorher zurück) und
> legt einen dritten Hardlink unter `users/<user>/<sid>.mp3` an, den
> `_entferne_datei_wenn_verwaist()` beim Löschen nicht mit abräumt. Bisher
> folgenlos (das Verzeichnis war leer, weil im Normalbetrieb fast alles
> über den Neu-Zweig läuft), aber jeder künftige Dedup-Upload hinterlässt
> einen verwaisten Hardlink. Server-Hygiene für Hermes, kein Datenverlust.
## Ziele ## Ziele
@@ -465,22 +483,23 @@ bei leerer lokaler Tabelle.
muss:** muss:**
- Stufe 1 (A1/A2/A3) **berührt den Löschpfad nicht** und hängt an keiner - Stufe 1 (A1/A2/A3) **berührt den Löschpfad nicht** und hängt an keiner
offenen Frage. Deshalb kann mit der Umsetzung begonnen werden, **ohne offenen Frage. Sie behebt außerdem als einzige einen belegten
dass die Antwort auf die A6-Rückfrage vorliegt**. Sie behebt außerdem Datenverlust-Bug — sie zuerst zu liefern ist auch inhaltlich richtig.
als einzige einen belegten Datenverlust-Bug — sie zuerst zu liefern - Der **Auswahl-Upload (Feature 3)** stand ursprünglich hinter der
ist auch inhaltlich richtig. A6-Klärung, weil er mehr Titeln eine cloudId gibt und damit die
- Der **Auswahl-Upload (Feature 3) kommt bewusst NACH der Klärung** der Angriffsfläche des Löschpfads vergrößert. **Dieser Grund ist seit dem
A6-Rückfrage: Er gibt mehr Titeln eine cloudId und vergrößert damit 2026-08-27 entfallen** (Löschpfad serverseitig entschärft, siehe
genau die Angriffsfläche des irreversiblen Löschpfads. Lautet die §ERLEDIGT). Die Stufe bleibt an ihrer Position, weil Stufe 1 den
Antwort „nein, nicht gewollt“, gehört der Server-Auftrag einzigen belegten Bug behebt und deshalb vorne gehört — nicht mehr,
(Datei-Wiederherstellung im Dedup-Zweig) davor. weil Stufe 2 blockiert wäre.
- Alles, was auf einen fremden Auftrag wartet (Server-Fix `:505`, - Alles, was auf einen fremden Auftrag wartet (`song_upload`-Event) oder
`song_upload`-Event) oder auf einen Build-Umbau auf einen Build-Umbau (`flutter_local_notifications`), steht hinten —
(`flutter_local_notifications`), steht hinten — das betrifft die das betrifft die **Stufen 46**. Innerhalb dieser drei blockiert keine
**Stufen 46**. Innerhalb dieser drei blockiert keine Stufe eine Stufe eine frühere.
frühere. Für Stufe 2 gilt das ausdrücklich **nicht**: sie wartet auf
die A6-Antwort (Absatz darüber), und Stufe 4 hängt zusätzlich am **Stand 2026-08-27: keine Stufe dieser Spec ist mehr blockiert.** Die
Server-Fix `:505` (§Offene Abhängigkeiten, Zeile 1). einzige offene Fremdabhängigkeit (`song_upload`-Event) betrifft
ausschließlich die spätere SSE-Stufe, die nicht Teil dieser Spec ist.
## Fehlerfälle — zu bauende Arbeit, keine Zusagen ## Fehlerfälle — zu bauende Arbeit, keine Zusagen
@@ -536,7 +555,15 @@ Verhalten im Einzelnen:
Bestandsfehler, von Feature 2 nicht angefasst, aber beim Testen Bestandsfehler, von Feature 2 nicht angefasst, aber beim Testen
präsent. präsent.
### Irreversibler Löschpfad (Bestand, nicht von dieser Spec verursacht — Backlog-P0) ### Löschpfad (war Backlog-P0 — am 2026-08-27 serverseitig entschärft)
**Der folgende Abschnitt beschreibt den Stand VOR dem Fix.** Die Kette
selbst besteht unverändert — eine verschwundene lokale Datei führt
weiterhin zur Server-Löschung. Was sich geändert hat: Sie ist nicht mehr
*irreversibel*. Erneutes Hochladen derselben Datei stellt sie jetzt in
Registry und Navidrome-Bibliothek wieder her (siehe §ERLEDIGT ganz oben).
Der letzte Satz des Kastens („Erneutes Hochladen repariert es **nicht**")
gilt daher **nicht mehr**.
> `markMissing` (`database.dart:237-245`) tombstoned jeden Titel, dessen > `markMissing` (`database.dart:237-245`) tombstoned jeden Titel, dessen
> Datei der Scan nicht findet — auch unbeabsichtigt (SD-Karte nicht > Datei der Scan nicht findet — auch unbeabsichtigt (SD-Karte nicht
@@ -552,8 +579,13 @@ Verhalten im Einzelnen:
> dauerhaft 404 (`:1400-1408`). Bisher nie ausgelöst (Live-DB: > dauerhaft 404 (`:1400-1408`). Bisher nie ausgelöst (Live-DB:
> `SUM(deleted)=0`). > `SUM(deleted)=0`).
Die zugehörige Rückfrage an Dustin ist **noch offen** — siehe §OFFENE Die zugehörige Rückfrage an Dustin ist **beantwortet und behoben**
ENTSCHEIDUNG ganz oben. siehe §ERLEDIGT ganz oben. Was bleibt: Die App tombstoned weiterhin
großzügig (nicht eingehängte SD-Karte genügt), und die Lösch-Bremse
greift weiterhin erst ab 109 von 325 Titeln. Beides ist App-Seite,
bewusst unangetastet und kein Blocker — der Schaden ist jetzt reparabel.
Ob die App zusätzlich vorsichtiger tombstonen soll, bleibt eine offene
Backlog-Frage.
### Verworfene Alternativen ### Verworfene Alternativen
@@ -638,7 +670,7 @@ Tests):
| # | Auftrag | Art | Blockiert | | # | Auftrag | Art | Blockiert |
|---|---|---|---| |---|---|---|---|
| 1 | `melo_cloud.py:505` — Owner-Prüfung in `handle_playlist_delete` | **Voraussetzung** | Feature 2 | | 1 | `melo_cloud.py:505` — Owner-Prüfung in `handle_playlist_delete` | **Voraussetzung** | Feature 2 |
| 2 | Datei-Wiederherstellung im Dedup-Zweig von `upload()` | **Voraussetzung, falls die Antwort auf die offene Entscheidung „nein" lautet** | Feature 3 | | 2 | ~~Datei-Wiederherstellung im Dedup-Zweig von `upload()`~~ | **ERLEDIGT 2026-08-27** (Hermes; end-to-end verifiziert) | nichts mehr |
| 3 | `song_upload`-SSE-Event | Voraussetzung für die spätere SSE-Stufe | nichts in dieser Spec | | 3 | `song_upload`-SSE-Event | Voraussetzung für die spätere SSE-Stufe | nichts in dieser Spec |
| 4 | Playlist-Rename-Endpunkt (`PUT`/`PATCH`) | optional | nichts (Feature 2 braucht ihn nicht mehr) | | 4 | Playlist-Rename-Endpunkt (`PUT`/`PATCH`) | optional | nichts (Feature 2 braucht ihn nicht mehr) |
| 5 | Playlist-Tombstones serverseitig | optional / Backlog | nichts | | 5 | Playlist-Tombstones serverseitig | optional / Backlog | nichts |
@@ -652,3 +684,16 @@ Tests):
beidseitig vorhanden, ohne dass der Server geändert werden müsste. beidseitig vorhanden, ohne dass der Server geändert werden müsste.
- `server_titel_screen._geladen` ist ein Einmal-Snapshot - `server_titel_screen._geladen` ist ein Einmal-Snapshot
(Bestandsfehler) — fällt beim Einbau des Einzel-Song-Knopfs auf. (Bestandsfehler) — fällt beim Einbau des Einzel-Song-Knopfs auf.
- **Server-Hygiene (2026-08-27 beim Löschpfad-Test gefunden):**
`_link_user()` wird nur im Dedup-Zweig von `upload()` aufgerufen (der
Neu-Zweig kehrt vorher zurück) und legt einen dritten Hardlink unter
`users/<user>/<sid>.mp3` an, den `_entferne_datei_wenn_verwaist()` beim
Löschen nicht mit abräumt. Bisher folgenlos — das Verzeichnis war leer,
weil im Normalbetrieb fast alles über den Neu-Zweig läuft. Seit dem
Wiederherstellungs-Fix hinterlässt aber jeder Dedup-Upload einen
verwaisten Hardlink. Auftrag an Hermes, kein Datenverlust, eilt nicht.
- **App-Seite, bewusst offen:** `markMissing` tombstoned großzügig (eine
nicht eingehängte SD-Karte genügt) und die Lösch-Bremse greift erst ab
109 von 325 Titeln. Seit dem Server-Fix ist der Schaden reparabel, also
kein Blocker — ob die App trotzdem vorsichtiger tombstonen soll, ist
eine eigene Entscheidung.