From 6a497138215fa51c393294494d60aa437d69a5f0 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Wed, 12 Aug 2026 08:03:04 +0200 Subject: [PATCH] feat: die Oberflaeche zu den vier Werkzeugen Bisher liessen sie sich nur ueber die API benutzen. Jetzt stehen sie dort, wo man sie sucht. SPEICHER bekommt zwei Karten: "Wozu fehlt ein Bild?" -- Muster und Namensliste hinein, beide Richtungen heraus. Das Muster kommt aus den ADRESSVORLAGEN und wird als Knopfreihe angeboten, damit hier nicht dieselbe Zeichenkette ein zweites Mal getippt wird. "Suchen und Ersetzen in Pfaden" -- und dort gibt es KEINEN Knopf, der es sofort tut. Erst zeigen, dann fragen, dann machen. Die Vorschau nennt die Zahl, die die Entscheidung traegt: wie oft die betroffenen Dateien zusammen geholt wurden. Null heisst, es schaut niemand zu; viertausend heisst, es schaut jemand zu. Gibt es auch nur ein Problem, verschwindet der Ausfuehren-Knopf ganz -- nicht ausgegraut, sondern weg, denn ein Problem verhindert ohnehin alles. EINSTELLUNGEN bekommt "Einstellungen mitnehmen". Herunterladen ist der harmlose Knopf; "mit Geheimnissen" ist rot und fragt nach, mit dem Satz, dass die Datei das Discord-Geheimnis und das Nextcloud-Passwort im Klartext traegt und der Download im Verwaltungs-Verlauf steht. TOKEN bekommen das Feld "Bilder verkleinern auf" -- im Anlege-Formular und im Bearbeiten-Dialog, mit dem Hinweis, dass kleinere Bilder NICHT aufgeblasen werden. In der Liste steht es als "max 512 px" neben den anderen Grenzen. Zwei eigene Stolperer beim Bauen, beide vom Uebersetzer gefangen: ein Patch brach vor dem Schreiben ab, sodass die Haelfte der API-Aufrufe fehlte -- und in FehlendeKarte blieb ein `note` stehen, das niemand mehr benutzte. Co-Authored-By: Claude Opus 5 --- server/ui/src/api.ts | 36 +++ server/ui/src/components/ErsetzenKarte.tsx | 232 +++++++++++++++++++ server/ui/src/components/FehlendeKarte.tsx | 174 ++++++++++++++ server/ui/src/components/MitnehmenKarte.tsx | 144 ++++++++++++ server/ui/src/components/TokenBearbeiten.tsx | 13 ++ server/ui/src/pages/Einstellungen.tsx | 3 + server/ui/src/pages/Speicher.tsx | 16 +- server/ui/src/pages/Tokens.tsx | 13 ++ 8 files changed, 630 insertions(+), 1 deletion(-) create mode 100644 server/ui/src/components/ErsetzenKarte.tsx create mode 100644 server/ui/src/components/FehlendeKarte.tsx create mode 100644 server/ui/src/components/MitnehmenKarte.tsx diff --git a/server/ui/src/api.ts b/server/ui/src/api.ts index d95256d..d9547e1 100644 --- a/server/ui/src/api.ts +++ b/server/ui/src/api.ts @@ -178,6 +178,8 @@ export type TokenRow = { max_bytes: number | null /** Wieviel dieser Token INSGESAMT halten darf. Null = unbegrenzt. */ quota_bytes: number | null + /** Groesste Kantenlaenge fuer Bilder. Null = ablegen wie es kommt. */ + max_kante: number | null /** Erlaubte Arten, mit Komma. Leer = alle. */ arten: string expires_at: number | null @@ -508,6 +510,38 @@ export const api = { { aktion }, ), + /** Wozu fehlt ein Bild — und was liegt da, ohne in der Liste zu stehen. */ + fehlende: (muster: string, namen: string) => + post<{ + muster: string + geprueft: number + da: number + fehlt: { name: string; pfad: string }[] + ueberzaehlig: string[] + ueberzaehligGesamt: number + unbrauchbar: { name: string; grund: string }[] + }>('/api/dash/fehlende', { muster, namen }), + + /** Ohne `ausfuehren` wird nur gerechnet und gezeigt — angefasst wird nichts. */ + ersetzen: (suchen: string, ersetzen: string, ordner: string, ausfuehren = false) => + post<{ + vorschau?: true + treffer?: number + plan?: { alt: string; neu: string; abrufe: number }[] + planGesamt?: number + probleme?: { alt: string; grund: string }[] + abrufeGesamt?: number + umbenannt?: number + gescheitert?: { alt: string; grund: string }[] + beispiele?: { alt: string; neu: string }[] + }>('/api/dash/media/ersetzen', { suchen, ersetzen, ordner, ausfuehren }), + + einstellungenImport: (werte: Record, ueberschreiben: boolean) => + post<{ gesetzt: string[]; uebersprungen: string[] }>('/api/dash/einstellungen/import', { + werte, + ueberschreiben, + }), + verwaltung: (limit = 100, offset = 0) => get<{ items: VerwaltungRow[]; total: number; limit: number; offset: number }>( `/api/dash/verwaltung?limit=${limit}&offset=${offset}`, @@ -534,6 +568,7 @@ export const api = { canDelete?: boolean maxMb?: string | null quotaMb?: string | null + maxKante?: string | null arten?: string[] | null gueltigTage?: string | null }, @@ -573,6 +608,7 @@ export const api = { prefix: string canDelete: boolean maxMb?: string + maxKante?: string quotaMb?: string arten?: string[] gueltigTage?: string diff --git a/server/ui/src/components/ErsetzenKarte.tsx b/server/ui/src/components/ErsetzenKarte.tsx new file mode 100644 index 0000000..060374b --- /dev/null +++ b/server/ui/src/components/ErsetzenKarte.tsx @@ -0,0 +1,232 @@ +import { useState } from 'react' +import { AlertTriangle, Eye, Replace } from 'lucide-react' +import { api, type Folders } from '../api' +import { run } from '../store' +import { Karte, Knopf, Nachfrage } from './Bausteine' + +type Vorschau = Awaited> + +/** Suchen und Ersetzen über viele Pfade. + * + * DIE VORSCHAU IST DER EIGENTLICHE TEIL, nicht die Bequemlichkeit dahinter. + * Was hier passiert, ist dieselbe Warnung wie beim Umbenennen eines Ordners — + * **jede alte Adresse gibt danach 404** —, nur mal tausend. Wer das ohne + * Gegenlesen auslöst, hat keine Gelegenheit mehr, es zu bemerken. + * + * Deshalb gibt es hier keinen Knopf, der es sofort tut. Erst zeigen, dann + * fragen, dann machen. */ +export function ErsetzenKarte({ + ordnerDaten, + fertig, +}: { + ordnerDaten: Folders | null + fertig: () => void +}) { + const [suchen, setSuchen] = useState('') + const [ersetzen, setErsetzen] = useState('') + const [ordner, setOrdner] = useState('') + const [vorschau, setVorschau] = useState(null) + const [laeuft, setLaeuft] = useState(false) + const [fragt, setFragt] = useState(false) + + const bereit = suchen.trim().length > 0 + const machbar = + vorschau?.vorschau === true && + (vorschau.probleme?.length ?? 0) === 0 && + (vorschau.planGesamt ?? 0) > 0 + + return ( + +

+ Für den Fall, dass sich eine Namenskonvention ändert oder ein Ordner anders heißen + soll. Erst wird gezeigt, was passieren würde{' '} + — angefasst wird nichts, bevor du es gelesen hast. +

+ +
+ + + +
+ +
+ { + setLaeuft(true) + const r = await run( + () => api.ersetzen(suchen, ersetzen, ordner, false), + (x) => + (x.probleme?.length ?? 0) > 0 + ? `${x.planGesamt} möglich, ${x.probleme?.length} nicht — siehe unten.` + : `${x.planGesamt} Datei(en) würden umbenannt.`, + ) + setLaeuft(false) + setVorschau(r) + }} + kind={ + <> + + Zeigen, was passieren würde + + } + /> + {machbar && ( + setFragt(true)} + kind={ + <> + + {vorschau?.planGesamt} umbenennen + + } + /> + )} +
+ + {vorschau?.vorschau && ( +
+

+ {vorschau.treffer} Treffer ·{' '} + {vorschau.planGesamt} umbenennbar ·{' '} + + {vorschau.probleme?.length ?? 0} + {' '} + Probleme +

+ + {(vorschau.abrufeGesamt ?? 0) > 0 && ( +

+ + + Die betroffenen Dateien wurden zusammen{' '} + {vorschau.abrufeGesamt}× geholt. Jede alte Adresse + gibt danach 404 — und keine davon meldet sich von selbst. + +

+ )} + + {(vorschau.probleme?.length ?? 0) > 0 && ( +
+

+ Das geht nicht — und deshalb wird gar nichts angefasst +

+
+                                {vorschau.probleme?.map((p) => `${p.alt}\n  → ${p.grund}`).join('\n')}
+                            
+
+ )} + + {(vorschau.plan?.length ?? 0) > 0 && ( +
+

+ So sähe es aus + {(vorschau.planGesamt ?? 0) > (vorschau.plan?.length ?? 0) && + ` — erste ${vorschau.plan?.length} von ${vorschau.planGesamt}`} +

+
+                                {vorschau.plan?.map((p) => `${p.alt}\n  → ${p.neu}`).join('\n')}
+                            
+
+ )} +
+ )} + + setFragt(false)} + onJa={async () => { + setLaeuft(true) + const r = await run( + () => api.ersetzen(suchen, ersetzen, ordner, true), + (x) => + `${x.umbenannt} umbenannt` + + ((x.gescheitert?.length ?? 0) > 0 + ? `, ${x.gescheitert?.length} nicht` + : '.'), + ) + setLaeuft(false) + setFragt(false) + if (r) { + setVorschau(null) + setSuchen('') + setErsetzen('') + fertig() + } + }} + text={ +

+ + + {suchen} wird zu{' '} + {ersetzen || '(nichts)'}, in{' '} + {vorschau?.planGesamt} Datei(en).{' '} + + Alle bisherigen Adressen dieser Dateien geben danach 404. + {' '} + Rückgängig geht das nur, indem du es andersherum noch einmal machst — + und das findet die Dateien nur, solange niemand sonst etwas geändert + hat. + +

+ } + /> +
+ ) +} diff --git a/server/ui/src/components/FehlendeKarte.tsx b/server/ui/src/components/FehlendeKarte.tsx new file mode 100644 index 0000000..d2a6e95 --- /dev/null +++ b/server/ui/src/components/FehlendeKarte.tsx @@ -0,0 +1,174 @@ +import { useEffect, useState } from 'react' +import { Check, ListChecks } from 'lucide-react' +import { api, type Vorlage } from '../api' +import { run } from '../store' +import { Karte, Knopf } from './Bausteine' + +/** Wozu fehlt ein Bild? + * + * DIE FRAGE, DIE BISHER NIEMAND BEANTWORTET HAT. Das Fotostudio weiß, was es + * aufgenommen hat. Dieser Dienst weiß, was liegt. Die Differenz rechnet + * keiner aus — und deshalb fällt „das Handy zeigt bei manchen Autos kein Bild" + * erst im Spiel auf. + * + * Das Muster kommt aus den **Adressvorlagen**, damit hier nicht dieselbe + * Zeichenkette ein zweites Mal getippt wird. Wer keine angelegt hat, tippt + * sie — aber der Vorschlag steht daneben. */ +export function FehlendeKarte() { + const [vorlagen, setVorlagen] = useState([]) + const [muster, setMuster] = useState('vehicles/{model}.webp') + const [namen, setNamen] = useState('') + const [laeuft, setLaeuft] = useState(false) + const [ergebnis, setErgebnis] = useState + > | null>(null) + + useEffect(() => { + api.einstellungen() + .then((e) => { + setVorlagen(e.vorlagen) + if (e.vorlagen[0]) setMuster(e.vorlagen[0].muster) + }) + .catch(() => { + /* dann eben ohne Vorschläge — das Feld ist trotzdem benutzbar */ + }) + }, []) + + const anzahl = namen.split(/[\r\n,;]+/).filter((n) => n.trim()).length + + return ( + +

+ Eine Liste von Namen hineingeben — Fahrzeugmodelle, Item-Namen — und beantwortet + bekommen, wozu kein Bild daliegt. Und + umgekehrt: was daliegt, ohne in der Liste zu stehen. +

+ +
+ + +