fix: unique, chronologically sortable result filenames
Two badges of the same person, same format, same day produced the same filename -- `2026-07-23_lea_the-frame.png` -- and the second upload silently overwrote the first at Picdrop. The name now carries the time down to the second and a short tag derived from the item id: 2026-08-20_143207_lea-badge_the-frame_a3f9.jpg The timestamp leads, so any file listing sorts chronologically on its own. The tag is derived, not random, so re-running the backfill is idempotent. Timestamps use Europe/Berlin (KLARBILD_TZ) rather than UTC -- a picture taken at 00:30 should not be dated to the previous day. Naming moved into its own dependency-free module so the rules are testable and identical everywhere. The generator strips its own stamp when a result is fed back in as a source, so names cannot accumulate. Backfill for the existing library in Admin -> Dateinamen: preview first, then apply. It re-stamps from `created_at` and the item id, keeping the motif and format already chosen, and leaves the stored files alone -- they live under their id, not their name. A separate button re-delivers everything under the new name; the old copies stay put at the destination, since deleting a customer's files automatically is the wrong reflex. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
This commit is contained in:
7 files changed
+460
-42
No files matched your search
@@ -10,6 +10,7 @@ export default function AdminApp() {
|
||||
const [toast, setToast] = useState<string | null>(null);
|
||||
const [orKey, setOrKey] = useState(''); const [pdPw, setPdPw] = useState(''); const [nasPw, setNasPw] = useState('');
|
||||
const [storage, setStorage] = useState<any>(null);
|
||||
const [rename, setRename] = useState<any>(null); // Probelauf/Ergebnis der Umbenennung
|
||||
const [targets, setTargets] = useState<any[]>([]);
|
||||
const [nt, setNt] = useState<any>({ protocol: 'sftp' });
|
||||
const notify = (t: string) => { setToast(t); setTimeout(() => setToast(null), 2600); };
|
||||
@@ -129,6 +130,36 @@ export default function AdminApp() {
|
||||
notify(r.deleted != null ? `${r.deleted} alte Positionen entfernt.` : r.purged != null ? `${r.purged} Quelldateien gelöscht.` : r.made != null ? `${r.made} Vorschaubilder erzeugt.` : (r.error || 'OK'));
|
||||
load();
|
||||
};
|
||||
// Eindeutige Dateinamen — erst zeigen, was passieren würde, dann anwenden.
|
||||
const renamePreview = async () => {
|
||||
notify('Prüfe Dateinamen …');
|
||||
const r = await fetch('/api/admin/storage', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'rename_preview' }) }).then((x) => x.json());
|
||||
if (r.error) { notify(r.error); return; }
|
||||
setRename(r);
|
||||
notify(r.umbenannt
|
||||
? `${r.umbenannt} von ${r.geprueft} Bildern bekämen einen neuen Namen${r.vorherDoppelt ? ` — ${r.vorherDoppelt} Namen sind aktuell doppelt vergeben` : ''}.`
|
||||
: `Alle ${r.geprueft} Namen sind bereits eindeutig.`);
|
||||
};
|
||||
const renameApply = async () => {
|
||||
if (!confirm('Alle Bilder der Bibliothek auf eindeutige Namen bringen?\n\nDie Dateien selbst bleiben unberührt — sie liegen unter ihrer ID im Speicher. '
|
||||
+ 'Bereits bei Picdrop abgelegte Kopien behalten ihren alten Namen; dafür gibt es danach „Erneut ausliefern".')) return;
|
||||
notify('Benenne um …');
|
||||
const r = await fetch('/api/admin/storage', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'rename_apply' }) }).then((x) => x.json());
|
||||
setRename(r);
|
||||
notify(r.error || `${r.umbenannt} Bilder umbenannt, ${r.unveraendert} waren schon in Ordnung.`);
|
||||
load();
|
||||
};
|
||||
const redeliverAll = async () => {
|
||||
if (!confirm('Alle bereits ausgelieferten Bilder erneut hochladen — unter dem neuen Namen?\n\n'
|
||||
+ 'Die alten Dateien bleiben am Ziel liegen und müssen dort von Hand entfernt werden. Das kann dauern.')) return;
|
||||
notify('Liefere erneut aus … (kann dauern)');
|
||||
const r = await fetch('/api/admin/storage', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'redeliver_all' }) }).then((x) => x.json());
|
||||
notify(r.versucht != null ? `${r.ok}/${r.versucht} erneut ausgeliefert${r.fehler ? `, ${r.fehler} fehlgeschlagen` : ''}.` : (r.error || 'Fehler'));
|
||||
load();
|
||||
};
|
||||
const mirrorAll = async () => {
|
||||
if (!s.nas_enabled) { notify('NAS erst aktivieren & speichern.'); return; }
|
||||
if (!confirm('Alle fertigen Bilder aufs NAS sichern?')) return;
|
||||
@@ -274,6 +305,38 @@ export default function AdminApp() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="karte">
|
||||
<div className="kopfzeile"><span className="mono-label">Dateinamen</span>
|
||||
<span className="fein">JJJJ-MM-TT_HHMMSS_motiv_format_kürzel</span></div>
|
||||
<div className="steuer">
|
||||
<div className="fein">Jedes Ergebnis bekommt einen Namen, der mit Datum und Uhrzeit beginnt — dadurch sortiert
|
||||
jede Dateiliste von allein chronologisch — und mit einem kurzen Kürzel endet, das aus der Bild-ID stammt.
|
||||
Damit können zwei gleichartige Bilder desselben Tages einander <b>nicht mehr überschreiben</b>.
|
||||
Beispiel: <code>2026-08-20_143207_lea-badge_the-frame_a3f9.jpg</code></div>
|
||||
<div className="reihe">
|
||||
<button className="mini" onClick={renamePreview}>Bibliothek prüfen</button>
|
||||
<button className="mini" onClick={renameApply}>Jetzt umbenennen</button>
|
||||
<button className="mini" onClick={redeliverAll}>Erneut ausliefern</button>
|
||||
</div>
|
||||
{rename && (
|
||||
<div className="fein">
|
||||
<b>{rename.geprueft} Bilder geprüft</b> · {rename.umbenannt} {rename.probelauf ? 'bekämen einen neuen Namen' : 'umbenannt'} ·
|
||||
{' '}{rename.unveraendert} unverändert{rename.vorherDoppelt ? ` · ${rename.vorherDoppelt} Namen waren doppelt vergeben` : ''}
|
||||
{rename.beispiele?.length ? (
|
||||
<ul className="namensliste">
|
||||
{rename.beispiele.map((b: any) => (
|
||||
<li key={b.id}><s>{b.alt}</s> → <code>{b.neu}</code></li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
<div className="fein">„Erneut ausliefern" lädt alle schon ausgelieferten Bilder unter dem neuen Namen erneut
|
||||
zum Ziel hoch. Die alten Dateien bleiben dort liegen — automatisch etwas beim Kunden zu löschen wäre der
|
||||
falsche Reflex; das machst du bei Picdrop selbst.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="karte">
|
||||
<div className="kopfzeile"><span className="mono-label">Synology-NAS (zweite Sicherung)</span>
|
||||
<div className="reihe"><button className="mini" onClick={netcheck}>Erreichbarkeit</button>
|
||||
@@ -464,6 +527,11 @@ function AdminStyles() {
|
||||
.zahlen div{background:var(--paper);border:1px solid var(--line);border-radius:3px;padding:11px;}
|
||||
.zahlen b{display:block;font-size:22px;line-height:1;}.zahlen span{font-size:11.5px;color:var(--soft);}
|
||||
.karte>.fein{padding:0 16px 14px;}
|
||||
/* Vorher/Nachher der Umbenennung — lange Namen dürfen die Karte nicht sprengen. */
|
||||
.namensliste{list-style:none;margin:8px 0 0;padding:0;display:grid;gap:4px;max-height:220px;overflow:auto;}
|
||||
.namensliste li{font-family:var(--font-mono);font-size:10.5px;line-height:1.5;overflow-wrap:anywhere;}
|
||||
.namensliste s{color:var(--soft);}
|
||||
.namensliste code{color:var(--ink);}
|
||||
.liste{padding:8px 16px 16px;display:flex;flex-direction:column;}
|
||||
.zeile{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--line);}
|
||||
.zeile b{font-size:14px;}
|
||||
|
||||
Reference in new issue
Block a user