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