From 8a17b8b9745252178b24441e8de22ee275df636c Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Sun, 23 Aug 2026 11:55:26 +0200 Subject: [PATCH] Loeschen loeschte nur die sichtbare Seite Gemeldet: alle 3565 Bilder eines Ordners gewaehlt, auf "3565 loeschen" gedrueckt -- weg waren 60. Sechzig ist die Seitengroesse, und darin steckt der ganze Fehler: onClick={() => setLoeschen(items.filter((i) => gewaehlt.has(i.id)))} `items` ist die OFFENE SEITE. Der Knopf zaehlte die Auswahl, geloescht wurde die Schnittmenge mit dem gerade Sichtbaren. Die Absicht dahinter war gut -- der Dialog will die Pfade zeigen, und die kennt er nur fuer geladene Eintraege -- aber das Ergebnis war eine Zahl, die etwas anderes versprach als sie tat. Jetzt haelt der Auftrag beides getrennt: `ids` sind ALLE Gewaehlten, `namen` nur die benennbaren. Der Dialog zeigt bis zu zwoelf Pfade und sagt "und N weitere" -- ohne eine Vollstaendigkeit vorzutaeuschen, die er nicht hat. IN HAEPPCHEN ZU 200. Der Dienst verschiebt jede Datei einzeln; 3565 in einer Anfrage laufen minutenlang und fallen beim Proxy in die Zeitgrenze -- dann waere die Haelfte weg und die Oberflaeche meldete einen Fehler. Nach jeder Runde steht im Knopf, wie weit es ist: bei etwas, das eine Minute dauert, ist Stillstand von Haengen nicht zu unterscheiden. Bricht eine Runde ab, wird abgebrochen und die Zahl genannt, die schon durch ist. UND DER WARNTEXT WAR FALSCH. Dort stand "Es gibt keine Sicherung, aus der sie zurueckkommt" -- aus der Zeit vor dem Papierkorb. Nachgesehen: `inPapierkorb` VERSCHIEBT die Datei und nimmt den Eintrag zurueck, wenn das Verschieben scheitert; 30 Tage Aufbewahrung. Eine Warnung, die schlimmer klingt als die Lage, haelt vom Aufraeumen ab und ist damit so schaedlich wie eine zu harmlose. Der Knopf heisst jetzt "In den Papierkorb". Co-Authored-By: Claude Opus 5 --- server/ui/src/pages/Galerie.tsx | 122 ++++++++++++++++++++++++-------- 1 file changed, 94 insertions(+), 28 deletions(-) diff --git a/server/ui/src/pages/Galerie.tsx b/server/ui/src/pages/Galerie.tsx index d4aec64..ee224d2 100644 --- a/server/ui/src/pages/Galerie.tsx +++ b/server/ui/src/pages/Galerie.tsx @@ -60,7 +60,16 @@ export function Galerie() { const [daten, setDaten] = useState(null) const [laedt, setLaedt] = useState(true) const [gewaehlt, setGewaehlt] = useState>(new Set()) - const [loeschen, setLoeschen] = useState(null) + /** Was geloescht werden soll. + * + * `ids` sind ALLE Gewaehlten, `namen` nur die, deren Pfad wir kennen -- + * also die der offenen Seite. Vorher war das eine einzige Liste, und + * genau daran ist es gescheitert: der Knopf zaehlte die Auswahl, geloescht + * wurde die Schnittmenge mit der SICHTBAREN Seite. Bei 3565 gewaehlten + * Bildern verschwanden 60. */ + const [loeschen, setLoeschen] = useState<{ ids: number[]; namen: MediaItem[] } | null>(null) + /** Wie weit das Loeschen ist -- bei tausenden Dateien dauert es. */ + const [loeschFortschritt, setLoeschFortschritt] = useState(0) const [loescht, setLoescht] = useState(false) // Welche Datei gerade gross zu sehen ist -- als ID und nicht als Objekt: // nach einem Neuladen der Liste zeigt sie sonst auf einen alten Datensatz. @@ -197,23 +206,53 @@ export function Galerie() { async function loeschenAusfuehren() { if (!loeschen) return setLoescht(true) + setLoeschFortschritt(0) - const ergebnis = - loeschen.length === 1 - ? await run( - () => api.deleteMedia(loeschen[0]!.id), - (r) => `${r.deleted} gelöscht.`, - ) - : await run( - () => api.deleteMediaBulk(loeschen.map((i) => i.id)), - (r) => - r.count === loeschen.length - ? `${r.count} Dateien gelöscht.` - : // Der Dienst überspringt, was er nicht findet. - // Diese Lücke zu verschweigen wäre genau die - // Sorte Rückmeldung, die hier nicht vorkommt. - `${r.count} von ${loeschen.length} gelöscht — der Rest war schon weg.`, - ) + // IN HAEPPCHEN, nicht in einem Zug. + // + // Der Dienst verschiebt jede Datei einzeln in den Papierkorb. Bei 3565 + // Bildern ist das eine Anfrage, die Minuten laeuft -- und die faellt + // beim Proxy in die Zeitgrenze, lange bevor sie fertig ist. Dann waere + // die Haelfte geloescht und die Oberflaeche meldete einen Fehler. + // + // 200 je Runde: gross genug, dass es nicht an der Anzahl der Anfragen + // haengt, klein genug fuer eine Antwort in Sekunden. Und nach jeder + // Runde steht im Knopf, wie weit es ist -- bei etwas, das eine Minute + // dauert, ist Stillstand nicht von Haengen zu unterscheiden. + const { note } = useStore.getState() + const PORTION = 200 + const alle = loeschen.ids + let geloescht = 0 + let abbruch: string | null = null + + for (let i = 0; i < alle.length; i += PORTION) { + const teil = alle.slice(i, i + PORTION) + try { + const r = teil.length === 1 + ? await api.deleteMedia(teil[0]!).then(() => ({ count: 1 })) + : await api.deleteMediaBulk(teil) + geloescht += r.count + } catch (e) { + // NICHT WEITERMACHEN. Wenn eine Runde scheitert, scheitern die + // naechsten vermutlich auch, und tausend Fehlermeldungen sagen + // weniger als eine mit der Zahl, die schon durch ist. + abbruch = e instanceof Error ? e.message : 'Unbekannt' + break + } + setLoeschFortschritt(geloescht) + } + + if (abbruch) { + note('fehler', `${geloescht} von ${alle.length} gelöscht, dann brach es ab: ${abbruch}`) + } else if (geloescht === alle.length) { + note('ok', `${geloescht} ${geloescht === 1 ? 'Datei' : 'Dateien'} in den Papierkorb verschoben.`) + } else { + // Der Dienst ueberspringt, was er nicht findet. Diese Luecke zu + // verschweigen waere genau die Sorte Rueckmeldung, die hier nicht + // vorkommt. + note('hinweis', `${geloescht} von ${alle.length} gelöscht — der Rest war schon weg.`) + } + const ergebnis = !abbruch setLoescht(false) setLoeschen(null) @@ -441,11 +480,23 @@ export function Galerie() { {gewaehlt.size > 0 && ( setLoeschen(items.filter((i) => gewaehlt.has(i.id)))} + onClick={() => + setLoeschen({ + // ALLE Gewaehlten, nicht nur die sichtbaren. + ids: [...gewaehlt], + namen: items.filter((i) => gewaehlt.has(i.id)), + }) + } kind={ <> - {gewaehlt.size} löschen + {/* WAEHREND ES LAEUFT die Zahl, die schon + durch ist. Bei tausenden Dateien dauert es + eine Minute, und Stillstand ist von Haengen + nicht zu unterscheiden. */} + {loescht && loeschFortschritt > 0 + ? `${loeschFortschritt} von ${gewaehlt.size} …` + : `${gewaehlt.size} löschen`} } /> @@ -611,7 +662,7 @@ export function Galerie() {