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,
0 Favoriten und 0 Playlisten auf dem Server — nichts überbauen.
- **Reihenfolge ist bindend.** Tasks 14 (Favoriten-Fix) hängen an nichts und
kommen zuerst. Tasks 57 sind **blockiert bis Dustin A6 beantwortet hat**
(irreversibler Löschpfad). Tasks 1012 sind **blockiert bis der Server-Fix
`melo_cloud.py:505` steht** bzw. bis Dustin entscheidet, ob Feature 2
überhaupt gebaut wird.
kommen zuerst. **Tasks 57 sind seit dem 2026-08-27 ENTBLOCKT** — der
Löschpfad ist serverseitig behoben und end-to-end verifiziert (siehe Spec,
§ERLEDIGT). Tasks 1012 bleiben blockiert, aber nur noch durch die
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`
> **BLOCKIERT bis Dustin A6 beantwortet hat.**
> A6 = „Ist der irreversible Löschpfad gewollt?" (Spec, §OFFENE ENTSCHEIDUNG).
> Dieser Task gibt mehr Titeln eine cloudId und vergrößert damit genau die
> Angriffsfläche des Löschpfads. Lautet die Antwort **„nein"**, gehört der
> Server-Auftrag „Datei-Wiederherstellung im Dedup-Zweig von `upload()`"
> **davor**. Lautet sie **„ja"**, darf sofort begonnen werden.
> **Nicht ohne Antwort anfangen.**
> ✅ **ENTBLOCKT seit 2026-08-27.** Die frühere Blockade lautete: dieser Task
> gibt mehr Titeln eine cloudId und vergrößert damit die Angriffsfläche des
> irreversiblen Löschpfads. Dustins Antwort auf A6 war „nein, nicht gewollt";
> Hermes hat den Fix am selben Tag eingebaut (Datei-Wiederherstellung im
> Dedup-Zweig von `upload()`), end-to-end verifiziert. Ein versehentlicher
> Löschvorgang ist jetzt durch erneutes Hochladen reparabel.
> **Darf gebaut werden.**
**Dateien:**
- Ä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"
> **BLOCKIERT bis Dustin A6 beantwortet hat** — dieselbe Begründung wie
> Task 5: die Aktion ist die Oberfläche zu `ladeAusgewaehlteHoch`.
> ✅ **ENTBLOCKT seit 2026-08-27** — dieselbe Begründung wie Task 5 (die
> Aktion ist die Oberfläche zu `ladeAusgewaehlteHoch`), und dieselbe
> Auflösung: Löschpfad serverseitig behoben. **Darf gebaut werden.**
**Dateien:**
- Ä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
> **BLOCKIERT bis Dustin A6 beantwortet hat** — nur, weil die Spec Stufe 2
> (Feature 3 **und** 4) als Ganzes hinter die A6-Antwort stellt.
> **Ehrlicher Hinweis für Dustin:** Die inhaltliche Begründung der Spec
> („vergrößert die Angriffsfläche des Löschpfads, weil mehr Titel eine cloudId
> bekommen") trifft auf **Feature 4 nicht zu** — ein Download vergibt keine
> 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.
> ✅ **ENTBLOCKT seit 2026-08-27** (Löschpfad serverseitig behoben). Der Task
> war ohnehin nur mitblockiert, weil die Spec Stufe 2 als Ganzes hinter die
> A6-Antwort stellte — die inhaltliche Begründung traf auf Feature 4 nie zu:
> ein Download vergibt keine cloudId und meldet nichts zum Server.
> **Darf gebaut werden.**
**Dateien:**
- Ä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
1. **A6 — irreversibler Löschpfad** (blockiert Tasks 57).
„Verschwindet eine lokale Datei, löscht der Abgleich sie am Server und aus
der Navidrome-Bibliothek; erneutes Hochladen repariert das nicht. Gewollt?"
*Nebenfrage:* Task 7 (Einzel-Song-Offline) vergibt keine cloudId und ist
von der Begründung sachlich nicht betroffen — darf er vorgezogen werden?
1. ~~**A6 — irreversibler Löschpfad**~~ ✅ **ERLEDIGT 2026-08-27.** Dustins
Antwort: nicht gewollt. Hermes hat die Datei-Wiederherstellung im
Dedup-Zweig von `upload()` eingebaut, end-to-end verifiziert (Upload →
Löschung → erneuter Upload stellt beide Kopien bitgenau wieder her).
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?
Heute 0 Playlisten am Server. Empfehlung: vertagen.
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
Status: Nachgeschärft nach agent-review-panel (Urteil Phase 14, 2026-08-27:
Score 6/10, Verdikt „Spec nachschärfen dann freigeben"). Alle 17
Aktionspunkte A1A17 sind eingearbeitet. Bedingung des Richters: diese
Fassung wird **einmal kurz gegengelesen**, bevor der Implementierungsplan
entsteht — A1, A5 und A7 ändern, *was* gebaut wird, nicht nur wie es
beschrieben ist.
Status: **Freigegeben, umsetzungsbereit** (2026-08-27).
Nachgeschärft nach agent-review-panel (Urteil Phase 14: Score 6/10,
Verdikt „Spec nachschärfen dann freigeben"), alle 17 Aktionspunkte
A1A17 eingearbeitet, Gegenlesen durch einen frischen Prüfer bestanden
(Bedingung des Richters erfüllt). Die einzige offene Frage (A6,
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
@@ -56,25 +58,41 @@ der Bau-Ansatz bleiben unangetastet):
- Die **Playlist-Konfliktregel** („längerer Stand gewinnt") entfällt
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?**
> Verschwindet eine lokale Datei (SD-Karte nicht eingehängt, Berechtigung
> entzogen, Dateimanager), löscht der Sync sie auf dem Server — und damit
> auch aus der Navidrome-Bibliothek. Erneutes Hochladen repariert das
> nachweislich **nicht**. Die technische Kette steht vollständig belegt
> unter §Risiken → „Irreversibler Löschpfad".
> **Die Rückfrage an Dustin („Ist der irreversible Löschpfad gewollt?")
> ist beantwortet: nein.** Hermes hat den Fix am selben Tag in
> `melo_cloud.py` eingebaut und den Dienst neu gestartet.
>
> - **Antwort „ja, gewollt“** → es ist eine dokumentierte Eigenschaft,
> nichts weiter zu tun.
> - **Antwort „nein“** → ein Server-Auftrag (Dedup-Zweig stellt die Datei
> wieder her, wenn `registry_pfad(sid)` leer ist) gehört **vor**
> Feature 3.
> **Was sich geändert hat:** Im Dedup-Zweig von `upload()` prüft der
> Server jetzt `registry_pfad(sid) is None` und stellt in diesem Fall die
> Datei aus den hochgeladenen Bytes wieder her (`shutil.move` nach `REG`
> plus `UPDATE registry SET path`), statt sie über `os.remove(tmp)` zu
> 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.
> **Stand: unbeantwortet.** Die Reihenfolge in §Reihenfolge ist bewusst
> so gewählt, dass mit Stufe 1 begonnen werden kann, ohne dass die
> Antwort vorliegt.
> **End-to-End verifiziert** (2026-08-27, eigene Testdatei, Fake-Nutzer,
> Testdaten danach restlos entfernt — Registry vor und nach dem Test bei
> 325 Titeln): Upload → beide Kopien da, Download bitgenau · Löschung →
> 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
@@ -465,22 +483,23 @@ bei leerer lokaler Tabelle.
muss:**
- 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
dass die Antwort auf die A6-Rückfrage vorliegt**. Sie behebt außerdem
als einzige einen belegten Datenverlust-Bug — sie zuerst zu liefern
ist auch inhaltlich richtig.
- Der **Auswahl-Upload (Feature 3) kommt bewusst NACH der Klärung** der
A6-Rückfrage: Er gibt mehr Titeln eine cloudId und vergrößert damit
genau die Angriffsfläche des irreversiblen Löschpfads. Lautet die
Antwort „nein, nicht gewollt“, gehört der Server-Auftrag
(Datei-Wiederherstellung im Dedup-Zweig) davor.
- Alles, was auf einen fremden Auftrag wartet (Server-Fix `:505`,
`song_upload`-Event) oder auf einen Build-Umbau
(`flutter_local_notifications`), steht hinten — das betrifft die
**Stufen 46**. Innerhalb dieser drei blockiert keine Stufe eine
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
Server-Fix `:505` (§Offene Abhängigkeiten, Zeile 1).
offenen Frage. Sie behebt außerdem als einzige einen belegten
Datenverlust-Bug — sie zuerst zu liefern ist auch inhaltlich richtig.
- Der **Auswahl-Upload (Feature 3)** stand ursprünglich hinter der
A6-Klärung, weil er mehr Titeln eine cloudId gibt und damit die
Angriffsfläche des Löschpfads vergrößert. **Dieser Grund ist seit dem
2026-08-27 entfallen** (Löschpfad serverseitig entschärft, siehe
§ERLEDIGT). Die Stufe bleibt an ihrer Position, weil Stufe 1 den
einzigen belegten Bug behebt und deshalb vorne gehört — nicht mehr,
weil Stufe 2 blockiert wäre.
- Alles, was auf einen fremden Auftrag wartet (`song_upload`-Event) oder
auf einen Build-Umbau (`flutter_local_notifications`), steht hinten —
das betrifft die **Stufen 46**. Innerhalb dieser drei blockiert keine
Stufe eine frühere.
**Stand 2026-08-27: keine Stufe dieser Spec ist mehr blockiert.** Die
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
@@ -536,7 +555,15 @@ Verhalten im Einzelnen:
Bestandsfehler, von Feature 2 nicht angefasst, aber beim Testen
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
> 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:
> `SUM(deleted)=0`).
Die zugehörige Rückfrage an Dustin ist **noch offen** — siehe §OFFENE
ENTSCHEIDUNG ganz oben.
Die zugehörige Rückfrage an Dustin ist **beantwortet und behoben**
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
@@ -638,7 +670,7 @@ Tests):
| # | Auftrag | Art | Blockiert |
|---|---|---|---|
| 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 |
| 4 | Playlist-Rename-Endpunkt (`PUT`/`PATCH`) | optional | nichts (Feature 2 braucht ihn nicht mehr) |
| 5 | Playlist-Tombstones serverseitig | optional / Backlog | nichts |
@@ -652,3 +684,16 @@ Tests):
beidseitig vorhanden, ohne dass der Server geändert werden müsste.
- `server_titel_screen._geladen` ist ein Einmal-Snapshot
(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.