From 3a098d708c470109cdf74814f30b63cc716a8433 Mon Sep 17 00:00:00 2001 From: Till Heidrich Date: Thu, 23 Jul 2026 20:52:48 +0000 Subject: [PATCH] feat: mirror-all-to-NAS, content reset (danger zone), in-app changelog + CHANGELOG.md, How-To refresh Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6 --- CHANGELOG.md | 39 ++++++++++++++++++++++++++++ src/components/AdminApp.tsx | 34 ++++++++++++++++++++++-- src/lib/maintenance.ts | 15 +++++++++++ src/pages/anleitung.astro | 22 ++++++++++++++-- src/pages/api/admin/reset.ts | 36 ++++++++++++++++++++++++++ src/pages/api/admin/storage.ts | 3 ++- src/pages/changelog.astro | 47 ++++++++++++++++++++++++++++++++++ 7 files changed, 191 insertions(+), 5 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/pages/api/admin/reset.ts create mode 100644 src/pages/changelog.astro diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a701dc4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# Changelog + +All notable changes to Klarbild are documented here. Newest first. + +## 2026-07-23 — Compose, generate & second backup + +### Added +- **Compose mode** — combine multiple images + a text description into one new image. +- **Generate mode** — create images from text only, no source needed. +- **The Frame "extend"** — outpaint more scene around the motif instead of only cropping. +- **Reuse** — take a finished result back into the studio as a new source to edit further. +- **Alternatives** — generate additional versions of the same image, grouped with a switcher. +- **Folders** — filter, rename and delete in the library; per-folder delivery gallery mapping. +- **Bulk actions** — download selected results as a ZIP, move several into a folder at once. +- **Thumbnails** — low-res previews for the library grid. +- **Models** — quality tiers incl. open-source (FLUX.2); the used model is shown per image. +- **Second backup** — mirror every result over SFTP/FTPS to a NAS (works over Tailscale too), + with a "NAS ✓" badge and a "mirror all existing images" action. +- **Storage management** — thumbnails, delete sources after processing, auto-retention. +- **Telegram** — fully button-driven (persistent keyboard + command menu): edit, compose, + "new image", reuse — no slash commands required. +- **Reset** — wipe library, jobs and folders cleanly (keeps users, settings, models, recipes), + guarded by a typed confirmation. +- **Help page**, in-app **changelog**, and a NAS **reachability diagnostic**. + +### Fixed / hardened +- Real per-image cost cap (monthly budget) enforced by the worker. +- Idempotent job completion (no duplicate delivery/notification). +- Before/after comparison works for composed images; hidden for text-only results. + +## 2026-07-22 — Foundation + +### Added +- Astro 5 + React islands UI (mobile-first), PostgreSQL, fs/S3 storage abstraction. +- pg-boss job queue with in-process worker; sharp pipeline (exact cm/dpi print sizes, + sticker contour). +- OpenRouter image gateway; admin-managed models. +- Picdrop delivery over SFTP/FTPS; recipes; library with before/after. +- Telegram bot (grammY webhook); argon2 auth; AES-256-GCM secret encryption. diff --git a/src/components/AdminApp.tsx b/src/components/AdminApp.tsx index 02486d7..af68200 100644 --- a/src/components/AdminApp.tsx +++ b/src/components/AdminApp.tsx @@ -80,6 +80,22 @@ export default function AdminApp() { notify(r.deleted != null ? `${r.deleted} alte Positionen entfernt.` : r.purged != null ? `${r.purged} Quelldateien gelöscht.` : (r.error || 'OK')); load(); }; + const mirrorAll = async () => { + if (!s.nas_enabled) { notify('NAS erst aktivieren & speichern.'); return; } + if (!confirm('Alle fertigen Bilder aufs NAS sichern?')) return; + notify('Sichere aufs NAS … (kann dauern)'); + const r = await fetch('/api/admin/storage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'mirror_all' }) }).then((x) => x.json()); + notify(r.total != null ? `${r.mirrored}/${r.total} aufs NAS gesichert${r.failed ? `, ${r.failed} fehlgeschlagen` : ''}.` : (r.error || 'Fehler')); + load(); + }; + const resetAll = async () => { + const t = prompt('ACHTUNG: Löscht ALLE Bilder, Aufträge und Ordner unwiderruflich (Nutzer, Einstellungen, Modelle & Rezepte bleiben).\n\nZum Bestätigen RESET eingeben:'); + if (t !== 'RESET') { if (t != null) notify('Abgebrochen — nicht bestätigt.'); return; } + notify('Setze zurück …'); + const r = await fetch('/api/admin/reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ confirm: 'RESET' }) }).then((x) => x.json()); + notify(r.ok ? `Zurückgesetzt: ${r.deleted_items} Bilder entfernt.` : (r.error || 'Fehler')); + load(); + }; const addModel = async (m: any) => { await fetch('/api/admin/models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_id: m.model_id, label: m.name, supports_alpha: m.supports_alpha, active: true }) }); @@ -200,8 +216,9 @@ export default function AdminApp() {
setNasPw(e.target.value)} />
-
field('nas_base_path', e.target.value)} />
-
Bei Synology am einfachsten SFTP aktivieren (Systemsteuerung → Terminal & SNMP → SFTP). Zugangsdaten werden verschlüsselt gespeichert.
+
field('nas_base_path', e.target.value)} />
+ +
Ergebnisse landen unter Basisordner / JJJJ-MM /. Zugangsdaten werden verschlüsselt gespeichert.
@@ -257,6 +274,15 @@ export default function AdminApp() { +
+
Zurücksetzen
+
+
Löscht alle Bilder, Aufträge und Ordner unwiderruflich, um sauber neu zu starten. + Nutzer, Einstellungen, Modelle und Rezepte bleiben erhalten. Tipp: vorher „Alle Bilder aufs NAS sichern".
+ +
+
+ {toast &&
{toast}
} @@ -275,6 +301,10 @@ function AdminStyles() { .input{width:100%;background:#fff;border:1px solid var(--line);border-radius:3px;padding:9px 11px;font-family:inherit;font-size:14px;color:var(--ink);} .fein{font-size:11.5px;color:var(--soft);line-height:1.5;} .knopf{border:none;border-radius:3px;padding:12px;cursor:pointer;background:var(--accent);color:#fff;font-family:inherit;font-weight:600;font-size:15px;} + .knopf.warn{background:var(--err);} + .karte.gefahr{border-color:color-mix(in oklab,var(--err) 40%,var(--line));} + .karte.gefahr .mono-label{color:var(--err);} + .steuer code{font-family:var(--font-mono);font-size:11px;background:var(--paper);border:1px solid var(--line);border-radius:3px;padding:1px 5px;} .zahlen{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;padding:16px;} .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);} diff --git a/src/lib/maintenance.ts b/src/lib/maintenance.ts index 780690a..0cb78a1 100644 --- a/src/lib/maintenance.ts +++ b/src/lib/maintenance.ts @@ -69,6 +69,21 @@ export async function purgeSources(): Promise<{ purged: number }> { return { purged }; } +/** Spiegelt alle fertigen Bilder aufs NAS (einmalige Nachsicherung). */ +export async function mirrorAllToNas(): Promise<{ mirrored: number; failed: number; total: number }> { + const { mirrorItemToNas, loadNasConfig } = await import('./nas'); + if (!(await loadNasConfig())) return { mirrored: 0, failed: 0, total: 0 }; + const rows = await query<{ id: string }>( + `SELECT id FROM items WHERE status='done' AND result_path IS NOT NULL + AND (nas_status IS NULL OR nas_status <> 'mirrored') ORDER BY created_at`); + let mirrored = 0, failed = 0; + for (const it of rows) { + const r = await mirrorItemToNas(it.id); + r.ok ? mirrored++ : failed++; + } + return { mirrored, failed, total: rows.length }; +} + let timer: NodeJS.Timeout | null = null; /** Täglicher Retention-Lauf (falls in den Einstellungen aktiviert). */ export function startMaintenance(): void { diff --git a/src/pages/anleitung.astro b/src/pages/anleitung.astro index 22aa6dc..2abe83d 100644 --- a/src/pages/anleitung.astro +++ b/src/pages/anleitung.astro @@ -5,7 +5,10 @@ import Base from '../layouts/Base.astro';

So funktioniert Klarbild

- +
+ Changelog + +

Aus Screenshots und Fundstücken werden druckfertige Bilder — bereinigt, freigestellt, im richtigen Format, optional direkt bei Picdrop. Alles geht über die Website und über Telegram.

@@ -48,6 +51,10 @@ import Base from '../layouts/Base.astro';
  • Weiterbearbeiten: übernimmt ein fertiges Bild als neue Vorlage ins Studio — dort erneut bearbeiten, ins nächste Format bringen oder im Modus Kombinieren mit weiteren Bildern mischen.
  • Vorher/Nachher: zeigt den Vergleich Vorlage ↔ Ergebnis (bei reinen Text-Bildern entfällt das).
  • +
  • Ordner verwalten: Ordner-Chip antippen, dann ✎ zum Umbenennen (inkl. Galerie) oder ✕ zum Löschen + (die Bilder bleiben erhalten).
  • +
  • Mehrere auf einmal: Bilder markieren, dann als ZIP laden oder gesammelt in einen Ordner + verschieben.
  • @@ -67,6 +74,16 @@ import Base from '../layouts/Base.astro';

    Für Profis gibt es zusätzlich das „/"-Befehlsmenü (/neu, /rezepte, /status) — nötig ist es nicht.

    +
    +

    6 · Sicherung & sauber neu starten (Admin)

    +

    Im Admin-Bereich lässt sich eine zweite Sicherung aufs NAS einrichten (SFTP/FTPS, auch über + Tailscale) — jedes fertige Bild wird dann zusätzlich gespiegelt und in der Bibliothek mit „NAS ✓" + markiert. Mit „Alle vorhandenen Bilder aufs NAS sichern" lassen sich auch ältere Bilder nachsichern.

    +

    Zum Aufräumen gibt es „Bibliothek & Aufträge zurücksetzen": löscht alle Bilder, Aufträge und Ordner + (Nutzer, Einstellungen, Modelle und Rezepte bleiben) — mit Sicherheitsabfrage. Am besten vorher einmal + aufs NAS sichern.

    +
    +

    Tipps

      @@ -81,7 +98,8 @@ import Base from '../layouts/Base.astro'; .howto{max-width:760px;margin:0 auto;} .hkopf{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;} .howto h1{font-family:var(--font-display);font-size:1.7rem;letter-spacing:-.02em;margin:6px 0 4px;} - .druck{background:var(--card);border:1px solid var(--line);border-radius:999px;padding:8px 15px;cursor:pointer;font-family:inherit;font-size:.85rem;color:var(--ink);} + .hkopf-akt{display:flex;gap:8px;align-items:center;} + .druck{background:var(--card);border:1px solid var(--line);border-radius:999px;padding:8px 15px;cursor:pointer;font-family:inherit;font-size:.85rem;color:var(--ink);text-decoration:none;} .lead{color:var(--soft);line-height:1.6;margin:4px 0 20px;} .howto section{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:16px 18px;margin-bottom:14px;} .howto h2{font-size:1.05rem;margin:0 0 10px;} diff --git a/src/pages/api/admin/reset.ts b/src/pages/api/admin/reset.ts new file mode 100644 index 0000000..fe6e165 --- /dev/null +++ b/src/pages/api/admin/reset.ts @@ -0,0 +1,36 @@ +import type { APIRoute } from 'astro'; +import { query } from '../../../lib/db'; +import { deleteObject } from '../../../lib/storage'; + +export const prerender = false; +const json = (b: unknown, s = 200) => + new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } }); + +/** Setzt Inhalte zurück: löscht alle Bilder/Aufträge/Ordner + zugehörige Dateien. + * Nutzer, Einstellungen, Modelle und Rezepte bleiben erhalten. + * Erfordert { confirm: 'RESET' }. */ +export const POST: APIRoute = async ({ request, locals }) => { + if (locals.user?.role !== 'admin') return new Response('Forbidden', { status: 403 }); + const b = await request.json().catch(() => ({} as any)); + if (b.confirm !== 'RESET') return json({ error: 'Bestätigung fehlt (confirm=RESET).' }, 400); + + // Zuerst alle Objekte aus dem Speicher entfernen. + const items = await query('SELECT source_path, source_paths, result_path, thumb_path FROM items'); + let objects = 0; + for (const it of items) { + const keys = [it.source_path, it.result_path, it.thumb_path, + ...((it.source_paths as string[]) || [])].filter(Boolean); + for (const k of keys) { await deleteObject(k).catch(() => {}); objects++; } + } + + // Inhalts-Tabellen leeren (Konfiguration bleibt). + await query('DELETE FROM print_job_items'); + await query('DELETE FROM print_jobs'); + await query('DELETE FROM share_links'); + await query('DELETE FROM telegram_drafts'); + await query('DELETE FROM items'); + await query('DELETE FROM jobs'); + await query('DELETE FROM folders'); + + return json({ ok: true, deleted_items: items.length, deleted_objects: objects }); +}; diff --git a/src/pages/api/admin/storage.ts b/src/pages/api/admin/storage.ts index ab2ca9d..f75c2ba 100644 --- a/src/pages/api/admin/storage.ts +++ b/src/pages/api/admin/storage.ts @@ -1,5 +1,5 @@ import type { APIRoute } from 'astro'; -import { storageStats, runRetention, purgeSources } from '../../../lib/maintenance'; +import { storageStats, runRetention, purgeSources, mirrorAllToNas } from '../../../lib/maintenance'; import { one } from '../../../lib/db'; export const prerender = false; @@ -12,6 +12,7 @@ export const GET: APIRoute = async () => json({ stats: await storageStats() }); export const POST: APIRoute = async ({ request }) => { const b = await request.json().catch(() => ({})); if (b.action === 'purge_sources') return json(await purgeSources()); + if (b.action === 'mirror_all') return json(await mirrorAllToNas()); if (b.action === 'retention') { const s = await one<{ retention_days: number | null }>('SELECT retention_days FROM settings WHERE id=1'); if (!s?.retention_days) return json({ error: 'Keine Aufbewahrungsfrist gesetzt.' }, 400); diff --git a/src/pages/changelog.astro b/src/pages/changelog.astro new file mode 100644 index 0000000..932fd62 --- /dev/null +++ b/src/pages/changelog.astro @@ -0,0 +1,47 @@ +--- +import Base from '../layouts/Base.astro'; +--- + +
      +
      +

      Changelog

      + ← Zur Hilfe +
      +

      Was in Klarbild neu ist. Neueste Änderungen oben.

      + +
      +
      23.07.2026

      Kombinieren, Erzeugen & zweite Sicherung

      +
        +
      • Kombinieren: mehrere Bilder + Beschreibung zu einem neuen Bild (z. B. „…​mit unserer Hündin Frieda").
      • +
      • Neu erzeugen: Bilder allein aus Text, ganz ohne Vorlage.
      • +
      • The Frame „Erweitern": Szene rund ums Motiv weiter generieren (Outpainting) statt nur zuschneiden.
      • +
      • Weiterbearbeiten: ein fertiges Bild als neue Vorlage übernehmen und erneut bearbeiten.
      • +
      • Alternativen: weitere Fassungen desselben Bildes erzeugen — gruppiert mit Umschalter.
      • +
      • Ordner: in der Bibliothek filtern, umbenennen, löschen; je Ordner eigene Picdrop-Galerie.
      • +
      • Sammel-Aktionen: mehrere Bilder als ZIP laden und auf einmal in einen Ordner verschieben.
      • +
      • Vorschaubilder für die Bibliothek (schnell & sparsam).
      • +
      • Modelle: Qualitätsstufen inkl. Open Source (FLUX.2); genutztes Modell wird angezeigt.
      • +
      • Zweite Sicherung (NAS): jedes Ergebnis zusätzlich per SFTP/FTPS aufs NAS (auch über Tailscale) — mit „NAS ✓"-Anzeige und „Alle Bilder aufs NAS sichern".
      • +
      • Speicherverwaltung: Vorschaubilder, Quellen löschen, automatische Aufbewahrungsfrist.
      • +
      • Telegram komplett knopfbasiert: Dauertastatur & Menü — Bearbeiten, Kombinieren, „✨ Neues Bild", Weiterbearbeiten, ganz ohne Befehle.
      • +
      • Zurücksetzen: Bibliothek & Aufträge sauber leeren (mit Sicherheitsabfrage).
      • +
      • Hilfe-Seite und dieser Changelog.
      • +
      • Unter der Haube: echter Kostendeckel, verlässlicher Auftragsabschluss, Erreichbarkeits-Diagnose fürs NAS.
      • +
      +
      +
      + + +