From ae91f4bfd5140caaacfd1245421fa26f7c648fe2 Mon Sep 17 00:00:00 2001 From: Till Heidrich Date: Fri, 24 Jul 2026 08:57:31 +0000 Subject: [PATCH] feat: llms.txt (AI-readable docs), job delete, friendly missing-source error - /llms.txt: public machine-readable capability/API/MCP doc for agents. - Job delete action + queue button for terminal jobs; cleans objects. - process.ts: clear 'Quelle nicht mehr vorhanden' message instead of raw EISDIR/ENOENT when a source was purged. - Docs corrected: Picdrop 'missing images' was web-UI sorting, not PNG; JPG delivery kept as size/perf improvement. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6 --- README.md | 8 +-- src/components/QueueApp.tsx | 7 +++ src/lib/process.ts | 15 +++++- src/middleware.ts | 2 +- src/pages/api/jobs/[id]/[action].ts | 13 +++++ src/pages/changelog.astro | 6 ++- src/pages/llms.txt.ts | 81 +++++++++++++++++++++++++++++ 7 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 src/pages/llms.txt.ts diff --git a/README.md b/README.md index a6b6526..eebd366 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,11 @@ Nummerierte SQL in `migrations/`, beim Start automatisch (kein ORM-Push). Wichti `settings.output_ext` / `recipes.output_ext`, `settings.*_metadata_sidecar`, `delivery_targets.*`. ## Stolpersteine & Lösungen (Learnings) -- **Picdrop zeigt große PNGs nicht:** Ergebnisse waren 12–32 MB PNG; Picdrop ist ein Foto-Proofing-Tool - und erwartet handliche JPGs. Lösung: nicht-transparente Ergebnisse als JPG ausliefern (`src/lib/delivery.ts`, - `deliverableBuffer`) + wählbares Ausgabeformat. Die Dateien lagen dabei **korrekt** per SFTP in `/POSTER LEA`. +- **„Bild bei Picdrop ✓, aber nicht in der Galerie":** Fehldiagnose vermeiden — die Dateien lagen die ganze + Zeit **korrekt** per SFTP in `/POSTER LEA` (per Admin → Picdrop → „Ordner anzeigen" verifizierbar). Der + eigentliche Grund war die **Sortierung in der Picdrop-Weboberfläche**, nicht das Format. Unabhängig davon + liefern wir nicht-transparente Ergebnisse jetzt als JPG aus (`src/lib/delivery.ts`, `deliverableBuffer`): + ~25 MB PNG → ~2 MB JPG, schneller und handlicher — plus wählbares Ausgabeformat (`output_ext`). - **„Unbekanntes Format: sticker5":** Seed legte ein Rezept mit Formatschlüssel `sticker5` an, den der Resolver nicht kannte → jeder Lauf schlug fehl. Lösung: generisches Parsen von `sticker`/`WxH`. - **Auftrag trotz Fehler grün:** `jobs.status` kannte kein `failed`; alle Positionen fehlgeschlagen wurde diff --git a/src/components/QueueApp.tsx b/src/components/QueueApp.tsx index 16e51fe..c0b4eda 100644 --- a/src/components/QueueApp.tsx +++ b/src/components/QueueApp.tsx @@ -78,6 +78,13 @@ export default function QueueApp({ jobId }: { jobId: string | null }) { {job?.status === 'paused' && } {failed > 0 && } {['queued', 'running', 'paused'].includes(job?.status) && } + {['done', 'failed', 'cancelled'].includes(job?.status) && ( + + )} diff --git a/src/lib/process.ts b/src/lib/process.ts index aad9ef9..e795a05 100644 --- a/src/lib/process.ts +++ b/src/lib/process.ts @@ -82,7 +82,20 @@ export async function processItem(itemId: string): Promise { mode === 'compose' ? (item.source_paths || []).filter(Boolean) : mode === 'generate' ? [] : (item.source_path ? [item.source_path] : []); - const sources = await Promise.all(sourceKeys.map((k) => getObject(k))); + // Braucht dieser Modus eine Quelle, ist aber keine (mehr) da? Klare Meldung statt roher fs-Fehler. + if (mode !== 'generate' && sourceKeys.length === 0) { + const e: any = new Error('Quelle nicht mehr vorhanden — bitte das Bild erneut hochladen.'); + e.friendly = 'Quelle nicht mehr vorhanden — bitte das Bild erneut hochladen.'; + throw e; + } + const sources = await Promise.all(sourceKeys.map(async (k) => { + try { return await getObject(k); } + catch { + const e: any = new Error('Quelldatei nicht mehr vorhanden — bitte das Bild erneut hochladen.'); + e.friendly = 'Quelldatei nicht mehr vorhanden — bitte das Bild erneut hochladen.'; + throw e; + } + })); const target = tasks.includes('format') && r.output_format ? resolveDimensions({ format: r.output_format, orientation: r.orientation, dpi }) diff --git a/src/middleware.ts b/src/middleware.ts index 0c662b9..8ac4c58 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -3,7 +3,7 @@ import { ensureInit } from './lib/init'; import { readSession } from './lib/auth'; import { one } from './lib/db'; -const PUBLIC_PATHS = [/^\/login/, /^\/api\/auth\/login/, /^\/api\/health/, /^\/g\//, /^\/api\/telegram\/webhook/]; +const PUBLIC_PATHS = [/^\/login/, /^\/api\/auth\/login/, /^\/api\/health/, /^\/g\//, /^\/api\/telegram\/webhook/, /^\/llms\.txt/]; /** API-Token (Authorization: Bearer …) → synthetischer Admin-Nutzer für /api/*. */ async function tokenUser(header: string | null): Promise { diff --git a/src/pages/api/jobs/[id]/[action].ts b/src/pages/api/jobs/[id]/[action].ts index fc015a6..ee4920f 100644 --- a/src/pages/api/jobs/[id]/[action].ts +++ b/src/pages/api/jobs/[id]/[action].ts @@ -34,6 +34,19 @@ export const POST: APIRoute = async ({ params, locals }) => { for (const it of items) await enqueue({ itemId: it.id, jobId: id }); break; } + case 'delete': { + // Auftrag samt Positionen und deren Objekten entfernen (für alte/fehlgeschlagene Aufträge). + const { deleteObject } = await import('../../../../lib/storage'); + const items = await query( + `SELECT source_path, source_paths, result_path, thumb_path FROM items WHERE job_id=$1`, [id]); + 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(() => {}); + } + await query(`DELETE FROM items WHERE job_id=$1`, [id]); + await query(`DELETE FROM jobs WHERE id=$1`, [id]); + break; + } default: return json({ error: 'Unbekannte Aktion.' }, 400); } diff --git a/src/pages/changelog.astro b/src/pages/changelog.astro index c8c7ae9..2ea03ab 100644 --- a/src/pages/changelog.astro +++ b/src/pages/changelog.astro @@ -10,9 +10,11 @@ import Base from '../layouts/Base.astro';

Was in Klarbild neu ist. Neueste Änderungen oben.

-
24.07.2026

Picdrop-Auslieferung, JPG, eigene Formate & fehlgeschlagene Aufträge

+
24.07.2026

JPG-Auslieferung, eigene Formate, fehlgeschlagene Aufträge & KI-Doku

    -
  • Picdrop bekommt jetzt JPG statt riesiger PNGs: Ergebnisse ohne Transparenz werden als handliches JPG (q92) ausgeliefert — ein 30×40-Poster schrumpft von ~25 MB auf ~2 MB. Freigestellte Motive (mit Transparenz) bleiben PNG.
  • +
  • Kleinere Dateien für Picdrop: Ergebnisse ohne Transparenz werden als handliches JPG (q92) ausgeliefert — ein 30×40-Poster schrumpft von ~25 MB auf ~2 MB. Freigestellte Motive (mit Transparenz) bleiben PNG. (Die zwischenzeitlich „verschwundenen" Bilder waren übrigens die ganze Zeit da — es lag an der Sortierung in der Picdrop-Weboberfläche.)
  • +
  • Auftrag löschen direkt aus der Warteschlange (für alte/fehlgeschlagene Aufträge). Fehlende Quellen melden jetzt einen klaren Hinweis statt kryptischer Technikfehler.
  • +
  • KI-lesbare Doku unter /llms.txt — beschreibt Zweck, Formate, API und MCP-Werkzeuge für Assistenten.
  • Dateiformat wählbar: global in den Admin-Einstellungen (Standard-Dateiformat PNG/JPG) und pro Umwandlung im Studio (Standard / PNG / JPG). JPG nur ohne Transparenz.
  • Eigene Formate: im Studio „Eigenes Maß (cm)" — einfach z. B. 25x35 eintippen; für Sticker reicht eine Zahl (5 = 5×5 cm). Presets speichern das mit.
  • Fehlgeschlagene Aufträge sind jetzt rot in der Übersicht (vorher grün) und tragen den Grund. Behoben: das Preset „Sticker 5 cm" lief ins Leere („Unbekanntes Format: sticker5").
  • diff --git a/src/pages/llms.txt.ts b/src/pages/llms.txt.ts new file mode 100644 index 0000000..ec922de --- /dev/null +++ b/src/pages/llms.txt.ts @@ -0,0 +1,81 @@ +import type { APIRoute } from 'astro'; + +export const prerender = false; + +/** + * Maschinen-/KI-lesbare Kurzdoku unter /llms.txt (öffentlich, ohne Geheimnisse). + * Beschreibt Zweck, Fähigkeiten und die HTTP-/MCP-Schnittstelle von Klarbild, + * damit ein KI-Agent das Tool ohne Weboberfläche bedienen kann. + */ +const BODY = `# Klarbild + +> Selbst gehostetes KI-Bildwerkzeug: verwandelt Screenshots und Fundstücke in +> saubere, druckfertige Bilder (bereinigen, freistellen, exakte Fotoformate, +> The-Frame-Querformat, Sticker mit Kontur), liefert an Picdrop (SFTP/FTPS) +> aus und sichert auf beliebige Backup-Ziele (z. B. NAS). Bedienung über Web, +> Telegram-Bot und HTTP-API/MCP. + +Basis-URL: https://klarbild.heidrich-digital.de +Sprache der Oberfläche: Deutsch. Stack: Astro 5 (SSR) · Postgres · sharp · pg-boss. + +## Authentifizierung (für Agenten) +- Header: \`Authorization: Bearer \` (Admin → „Automatisierung / MCP-Zugriff"). +- Der Token gilt für \`/api/*\` — NICHT für \`/api/admin/*\` (nur per Login-Session). +- Ohne Token/Session antworten API-Routen mit 401. + +## Kernbegriffe +- Modus (mode): \`each\` (Screenshot bereinigen/freistellen/formatieren), + \`compose\` (ein Bild umwandeln ODER mehrere kombinieren, mit Textbeschreibung), + \`generate\` (neues Bild allein aus Text). +- Aufgaben (tasks, nur bei each): \`clean\`, \`cutout\`, \`format\`, \`contour\`. +- Rezept (recipe): gespeicherte Voreinstellung. Auftrag (job): ein Stapel; Position (item): ein Bild. +- Auftrags-Status: queued, running, paused, done, failed, cancelled. \`failed\` = alle Positionen fehlgeschlagen. + +## Formate (output_format) +- Feste Schlüssel: 9x13,10x15,13x18,15x20,20x30,30x40,30x45,40x50,40x60,50x70,60x90, + A4,A3,A2,20x20,30x30, theframe (3840x2160), hochformat (2160x3840), keep (Original behalten). +- Freie Formate ohne Vorgabe: \`BREITExHOEHE\` in cm (z. B. \`25x35\`) oder \`sticker\` + für N×N cm (z. B. \`sticker5\` = 5×5 cm). Grenze 300 cm. Exakte Pixel: round(cm/2.54*dpi). +- Dateiformat (output_ext): \`png\` (verlustfrei, Transparenz) oder \`jpg\` (klein, Picdrop-freundlich). + JPG nur ohne Transparenz; freigestellte Motive bleiben immer PNG. Global (Admin) oder pro Rezept. + +## HTTP-API (Auswahl, JSON) +- GET /api/health — Status (öffentlich). +- GET /api/me — aktueller Nutzer/Rechte. +- GET /api/models — verfügbare Bildmodelle (Qualitätsstufen). +- GET /api/recipes — Rezepte/Presets auflisten. +- POST /api/recipes — Rezept anlegen. Body u. a.: name, mode, tasks[], output_format, + orientation(portrait|landscape), crop_mode(crop|extend), dpi, contour_mm, output_ext(png|jpg|null), + delivery(library|picdrop|both), picdrop_gallery, delivery_target_id. +- DELETE /api/recipes/:id — Rezept löschen. +- GET /api/delivery-targets — Auslieferungs-/Backup-Ziele. +- POST /api/jobs — Auftrag starten. Body: { recipe, mode, delivery, private, + prompt_text?(compose/generate), sources:[{source_path, filename}] }. Antwort: { jobId }. +- GET /api/jobs — letzte Aufträge. GET /api/jobs/:id — Auftrag + Positionen (inkl. error_message, finished_at). +- POST /api/jobs/:id/{pause|resume|cancel|retry-failed|delete}. +- POST /api/items/:id/{retry|reuse|alternative}. +- GET /api/items/:id/file — Ergebnisbild. Query: ?thumb=1 (Vorschau), ?preview=1 (kleines JPG fürs Vollbild), + ?download=1 (als Datei), ?src=1 (Quelle). Content-Type wird aus den Magic Bytes bestimmt (PNG/JPG/WEBP). + +## MCP-Server (für Assistenten) +Werkzeug \`klarbild\` (mcp/klarbild-mcp.mjs), nutzt denselben API-Token: +- list_recipes — verfügbare Rezepte. +- process_images — Bilder (lokaler Ordner / URLs) mit einem Rezept verarbeiten lassen. +- job_status — Fortschritt/Ergebnis eines Auftrags abfragen. + +## Auslieferung +- Picdrop = SFTP/FTPS-Ziel; Galerie = Unterordner unter dem Basispfad. Voller Pfad = Basisordner + Galerie + (wird erst beim Ausliefern zusammengesetzt). Standard-Galerie „POSTER LEA"; „The Frame" → „TheFrame-Backgrounds". +- Nicht-transparente Ergebnisse gehen als JPG an Picdrop (kleiner, zuverlässiger). +- Optionaler .md-Metadaten-Beileger je Ziel schaltbar (Picdrop/NAS/Zusatzziel). + +## Datenschutz +- Private Session (job.private=true): kein Bibliothekseintrag, keine Auslieferung/Sicherung, + kein gespeicherter Prompt; Ergebnis wird nach kurzer Zeit gelöscht. +- Sichtbarkeit global steuerbar (nur eigene / alle sehen alles); Admin sieht alles. + +Weitere Doku (menschlich): In-App /anleitung und /changelog. +`; + +export const GET: APIRoute = async () => + new Response(BODY, { headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'public, max-age=3600' } });