feat: mirror-all-to-NAS, content reset (danger zone), in-app changelog + CHANGELOG.md, How-To refresh
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
@@ -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.
|
||||||
@@ -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'));
|
notify(r.deleted != null ? `${r.deleted} alte Positionen entfernt.` : r.purged != null ? `${r.purged} Quelldateien gelöscht.` : (r.error || 'OK'));
|
||||||
load();
|
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) => {
|
const addModel = async (m: any) => {
|
||||||
await fetch('/api/admin/models', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
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 }) });
|
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() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="feld"><label>Passwort {s.nas_password_set && <em>(gesetzt)</em>}</label>
|
<div className="feld"><label>Passwort {s.nas_password_set && <em>(gesetzt)</em>}</label>
|
||||||
<input className="input" type="password" placeholder={s.nas_password_set ? '••••••' : ''} value={nasPw} onChange={(e) => setNasPw(e.target.value)} /></div>
|
<input className="input" type="password" placeholder={s.nas_password_set ? '••••••' : ''} value={nasPw} onChange={(e) => setNasPw(e.target.value)} /></div>
|
||||||
<div className="feld"><label>Basisordner auf dem NAS</label><input className="input" placeholder="/homes/klarbild oder /volume1/…" value={s.nas_base_path ?? ''} onChange={(e) => field('nas_base_path', e.target.value)} /></div>
|
<div className="feld"><label>Basisordner auf dem NAS</label><input className="input" placeholder="/Klarbild" value={s.nas_base_path ?? ''} onChange={(e) => field('nas_base_path', e.target.value)} /></div>
|
||||||
<div className="fein">Bei Synology am einfachsten SFTP aktivieren (Systemsteuerung → Terminal & SNMP → SFTP). Zugangsdaten werden verschlüsselt gespeichert.</div>
|
<button className="mini" onClick={mirrorAll}>Alle vorhandenen Bilder aufs NAS sichern</button>
|
||||||
|
<div className="fein">Ergebnisse landen unter <code>Basisordner / JJJJ-MM /</code>. Zugangsdaten werden verschlüsselt gespeichert.</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -257,6 +274,15 @@ export default function AdminApp() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="karte gefahr">
|
||||||
|
<div className="kopfzeile"><span className="mono-label">Zurücksetzen</span></div>
|
||||||
|
<div className="steuer">
|
||||||
|
<div className="fein">Löscht <b>alle Bilder, Aufträge und Ordner</b> unwiderruflich, um sauber neu zu starten.
|
||||||
|
Nutzer, Einstellungen, Modelle und Rezepte bleiben erhalten. Tipp: vorher „Alle Bilder aufs NAS sichern".</div>
|
||||||
|
<button className="knopf warn" onClick={resetAll}>Bibliothek & Aufträge zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{toast && <div className="toast">{toast}</div>}
|
{toast && <div className="toast">{toast}</div>}
|
||||||
<AdminStyles />
|
<AdminStyles />
|
||||||
</div>
|
</div>
|
||||||
@@ -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);}
|
.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;}
|
.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{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{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 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);}
|
.zahlen b{display:block;font-size:22px;line-height:1;}.zahlen span{font-size:11.5px;color:var(--soft);}
|
||||||
|
|||||||
@@ -69,6 +69,21 @@ export async function purgeSources(): Promise<{ purged: number }> {
|
|||||||
return { purged };
|
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;
|
let timer: NodeJS.Timeout | null = null;
|
||||||
/** Täglicher Retention-Lauf (falls in den Einstellungen aktiviert). */
|
/** Täglicher Retention-Lauf (falls in den Einstellungen aktiviert). */
|
||||||
export function startMaintenance(): void {
|
export function startMaintenance(): void {
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import Base from '../layouts/Base.astro';
|
|||||||
<div class="howto">
|
<div class="howto">
|
||||||
<div class="hkopf">
|
<div class="hkopf">
|
||||||
<h1>So funktioniert Klarbild</h1>
|
<h1>So funktioniert Klarbild</h1>
|
||||||
<button class="druck" onclick="window.print()">Als PDF / drucken</button>
|
<div class="hkopf-akt">
|
||||||
|
<a class="druck" href="/changelog">Changelog</a>
|
||||||
|
<button class="druck" onclick="window.print()">Als PDF / drucken</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="lead">Aus Screenshots und Fundstücken werden druckfertige Bilder — bereinigt, freigestellt,
|
<p class="lead">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.</p>
|
im richtigen Format, optional direkt bei Picdrop. Alles geht über die Website und über Telegram.</p>
|
||||||
@@ -48,6 +51,10 @@ import Base from '../layouts/Base.astro';
|
|||||||
<li><b>Weiterbearbeiten</b>: übernimmt ein fertiges Bild als neue Vorlage ins Studio — dort erneut
|
<li><b>Weiterbearbeiten</b>: ü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.</li>
|
bearbeiten, ins nächste Format bringen oder im Modus Kombinieren mit weiteren Bildern mischen.</li>
|
||||||
<li><b>Vorher/Nachher</b>: zeigt den Vergleich Vorlage ↔ Ergebnis (bei reinen Text-Bildern entfällt das).</li>
|
<li><b>Vorher/Nachher</b>: zeigt den Vergleich Vorlage ↔ Ergebnis (bei reinen Text-Bildern entfällt das).</li>
|
||||||
|
<li><b>Ordner verwalten</b>: Ordner-Chip antippen, dann ✎ zum Umbenennen (inkl. Galerie) oder ✕ zum Löschen
|
||||||
|
(die Bilder bleiben erhalten).</li>
|
||||||
|
<li><b>Mehrere auf einmal</b>: Bilder markieren, dann als <b>ZIP laden</b> oder gesammelt <b>in einen Ordner</b>
|
||||||
|
verschieben.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -67,6 +74,16 @@ import Base from '../layouts/Base.astro';
|
|||||||
<p class="mini">Für Profis gibt es zusätzlich das „/"-Befehlsmenü (/neu, /rezepte, /status) — nötig ist es nicht.</p>
|
<p class="mini">Für Profis gibt es zusätzlich das „/"-Befehlsmenü (/neu, /rezepte, /status) — nötig ist es nicht.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>6 · Sicherung & sauber neu starten (Admin)</h2>
|
||||||
|
<p>Im Admin-Bereich lässt sich eine <b>zweite Sicherung aufs NAS</b> einrichten (SFTP/FTPS, auch über
|
||||||
|
Tailscale) — jedes fertige Bild wird dann zusätzlich gespiegelt und in der Bibliothek mit <b>„NAS ✓"</b>
|
||||||
|
markiert. Mit <b>„Alle vorhandenen Bilder aufs NAS sichern"</b> lassen sich auch ältere Bilder nachsichern.</p>
|
||||||
|
<p>Zum Aufräumen gibt es <b>„Bibliothek & Aufträge zurücksetzen"</b>: löscht alle Bilder, Aufträge und Ordner
|
||||||
|
(Nutzer, Einstellungen, Modelle und Rezepte bleiben) — mit Sicherheitsabfrage. Am besten vorher einmal
|
||||||
|
aufs NAS sichern.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Tipps</h2>
|
<h2>Tipps</h2>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -81,7 +98,8 @@ import Base from '../layouts/Base.astro';
|
|||||||
.howto{max-width:760px;margin:0 auto;}
|
.howto{max-width:760px;margin:0 auto;}
|
||||||
.hkopf{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;}
|
.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;}
|
.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;}
|
.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 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;}
|
.howto h2{font-size:1.05rem;margin:0 0 10px;}
|
||||||
|
|||||||
@@ -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<any>('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 });
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { APIRoute } from 'astro';
|
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';
|
import { one } from '../../../lib/db';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
@@ -12,6 +12,7 @@ export const GET: APIRoute = async () => json({ stats: await storageStats() });
|
|||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
const b = await request.json().catch(() => ({}));
|
const b = await request.json().catch(() => ({}));
|
||||||
if (b.action === 'purge_sources') return json(await purgeSources());
|
if (b.action === 'purge_sources') return json(await purgeSources());
|
||||||
|
if (b.action === 'mirror_all') return json(await mirrorAllToNas());
|
||||||
if (b.action === 'retention') {
|
if (b.action === 'retention') {
|
||||||
const s = await one<{ retention_days: number | null }>('SELECT retention_days FROM settings WHERE id=1');
|
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);
|
if (!s?.retention_days) return json({ error: 'Keine Aufbewahrungsfrist gesetzt.' }, 400);
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
import Base from '../layouts/Base.astro';
|
||||||
|
---
|
||||||
|
<Base title="Changelog · Klarbild">
|
||||||
|
<div class="clog">
|
||||||
|
<div class="ckopf">
|
||||||
|
<h1>Changelog</h1>
|
||||||
|
<a class="zurueck" href="/anleitung">← Zur Hilfe</a>
|
||||||
|
</div>
|
||||||
|
<p class="lead">Was in Klarbild neu ist. Neueste Änderungen oben.</p>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="ver"><span class="tag">23.07.2026</span><h2>Kombinieren, Erzeugen & zweite Sicherung</h2></div>
|
||||||
|
<ul>
|
||||||
|
<li><b>Kombinieren:</b> mehrere Bilder + Beschreibung zu einem neuen Bild (z. B. „…mit unserer Hündin Frieda").</li>
|
||||||
|
<li><b>Neu erzeugen:</b> Bilder allein aus Text, ganz ohne Vorlage.</li>
|
||||||
|
<li><b>The Frame „Erweitern":</b> Szene rund ums Motiv weiter generieren (Outpainting) statt nur zuschneiden.</li>
|
||||||
|
<li><b>Weiterbearbeiten:</b> ein fertiges Bild als neue Vorlage übernehmen und erneut bearbeiten.</li>
|
||||||
|
<li><b>Alternativen:</b> weitere Fassungen desselben Bildes erzeugen — gruppiert mit Umschalter.</li>
|
||||||
|
<li><b>Ordner:</b> in der Bibliothek filtern, umbenennen, löschen; je Ordner eigene Picdrop-Galerie.</li>
|
||||||
|
<li><b>Sammel-Aktionen:</b> mehrere Bilder als ZIP laden und auf einmal in einen Ordner verschieben.</li>
|
||||||
|
<li><b>Vorschaubilder</b> für die Bibliothek (schnell & sparsam).</li>
|
||||||
|
<li><b>Modelle:</b> Qualitätsstufen inkl. Open Source (FLUX.2); genutztes Modell wird angezeigt.</li>
|
||||||
|
<li><b>Zweite Sicherung (NAS):</b> jedes Ergebnis zusätzlich per SFTP/FTPS aufs NAS (auch über Tailscale) — mit „NAS ✓"-Anzeige und „Alle Bilder aufs NAS sichern".</li>
|
||||||
|
<li><b>Speicherverwaltung:</b> Vorschaubilder, Quellen löschen, automatische Aufbewahrungsfrist.</li>
|
||||||
|
<li><b>Telegram komplett knopfbasiert:</b> Dauertastatur & Menü — Bearbeiten, Kombinieren, „✨ Neues Bild", Weiterbearbeiten, ganz ohne Befehle.</li>
|
||||||
|
<li><b>Zurücksetzen:</b> Bibliothek & Aufträge sauber leeren (mit Sicherheitsabfrage).</li>
|
||||||
|
<li><b>Hilfe-Seite</b> und dieser Changelog.</li>
|
||||||
|
<li>Unter der Haube: echter Kostendeckel, verlässlicher Auftragsabschluss, Erreichbarkeits-Diagnose fürs NAS.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.clog{max-width:760px;margin:0 auto;}
|
||||||
|
.ckopf{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;}
|
||||||
|
.clog h1{font-family:var(--font-display);font-size:1.7rem;letter-spacing:-.02em;margin:6px 0 4px;}
|
||||||
|
.zurueck{color:var(--accent);text-decoration:none;font-size:.9rem;}
|
||||||
|
.lead{color:var(--soft);margin:4px 0 20px;}
|
||||||
|
.clog section{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:16px 18px;margin-bottom:14px;}
|
||||||
|
.ver{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap;}
|
||||||
|
.ver h2{font-size:1.05rem;margin:0;}
|
||||||
|
.tag{font-family:var(--font-mono);font-size:10px;letter-spacing:.08em;background:var(--accent-bg);color:var(--accent);padding:3px 9px;border-radius:20px;}
|
||||||
|
.clog ul{margin:0;padding-left:20px;line-height:1.65;}
|
||||||
|
.clog li{margin-bottom:6px;}
|
||||||
|
</style>
|
||||||
|
</Base>
|
||||||
Reference in New Issue
Block a user