feat(cms): Aufraeumen von Uploads, Texte-Bearbeitung und PDF-Slot

Bilder wurden nie geloescht - weder beim Loeschen eines Datensatzes noch
beim Austauschen. Dazu waren die Textbereiche zwar im Backend vorhanden,
aber ohne Oberflaeche, und die Getraenkekarte hing hartkodiert im Markup.

Aufraeumen:
- asset.service.ts loescht ausschliesslich Dateien, die das CMS selbst
  angelegt hat (Muster <zeitstempel>-<zufall>.<ext>) und nur innerhalb der
  Upload-Ordner. Handgepflegte Assets wie event_karaoke.jpg oder Welcome.png
  bleiben unangetastet, auch wenn sie nirgends referenziert sind.
- image-refs.service.ts sammelt alle benutzten Bild-URLs, inklusive der
  Pfade aus den Textbereichen. Geloescht wird nur, was wirklich niemand
  mehr benutzt - ein von zwei Events geteiltes Bild bleibt liegen.
- Events und Gallery raeumen beim Loeschen und beim Bildwechsel mit auf.
- Der Publish entfernt zusaetzlich Verwaiste. Die Referenzliste deckt
  bewusst alle Zeilen ab, auch unveroeffentlichte, sonst verlieren die ihr
  Bild. Die Loeschungen werden mitcommittet, das Repo schrumpft also.

Texte:
- Neuer Adminbereich fuer Hero, Willkommen und Drinks ueber die schon
  vorhandenen /api/content-Endpunkte, samt Highlights-Liste und Bildern.
- Der Generator maskiert Texte jetzt. Ohne das haette ein "<" oder "&" in
  einem Feld gereicht, um den Astro-Build und damit den Deploy zu killen.

PDF:
- POST /api/pdf/drinks nimmt die Getraenkekarte entgegen, hinterlegt die
  URL in der drinks-Section und raeumt den Vorgaenger weg. Der Generator
  verlinkt sie, statt den Pfad fest im Markup zu haben.

Ausserdem:
- Vorschaubilder im Admin auf eine feste Box gezwungen. Uploads sind bis
  1600px breit und haben das Layout je nach Seitenverhaeltnis zerrissen.
- git.service.ts sichert public/pdf beim Neu-Clone mit weg, nicht nur
  public/images.
- Der Publish scheitert nicht mehr, wenn nur das Audit-Log nicht
  geschrieben werden kann - der Push ist da laengst durch.
- Upload-Logik lag dreifach kopiert vor, jetzt in upload.service.ts. Der
  Helfer prueft auch auf abgeschnittene Dateien; bisher landete bei zu
  grossen Uploads ein kaputtes Bild auf der Platte.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
2026-08-11 11:42:59 +02:00
co-authored by Claude Opus 5
parent 2af70c238b
commit 60246f3941
12 changed files with 893 additions and 138 deletions
+44
View File
@@ -3,6 +3,8 @@ import { z } from 'zod';
import { db } from '../config/database.js';
import { contentSections } from '../db/schema.js';
import { eq } from 'drizzle-orm';
import { saveUploadedImage } from '../services/upload.service.js';
import { dropImageIfUnused, extractImageUrls } from '../services/image-refs.service.js';
// Fastify JSON schema for content section body
const contentBodyJsonSchema = {
@@ -78,6 +80,20 @@ const contentRoute: FastifyPluginAsync = async (fastify) => {
.returning();
}
// Bilder, die durch die Aenderung herausgefallen sind, wegraeumen
const before = extractImageUrls((existing as any)?.contentJson);
const after = extractImageUrls(result.contentJson);
for (const url of before) {
if (after.has(url)) continue;
try {
if (await dropImageIfUnused(url)) {
fastify.log.info(`Removed unused content image ${url}`);
}
} catch (err) {
fastify.log.warn({ err }, 'Could not remove unused content image');
}
}
return {
section: result.sectionName,
content: result.contentJson,
@@ -85,6 +101,34 @@ const contentRoute: FastifyPluginAsync = async (fastify) => {
};
});
// Bild fuer einen Textbereich hochladen (Welcome-Bild, Monatshit, Whiskey)
fastify.post('/content/upload', {
preHandler: [fastify.authenticate],
}, async (request, reply) => {
try {
const file = await (request as any).file();
if (!file) {
return reply.code(400).send({ error: 'No file uploaded' });
}
const mime = file.mimetype as string | undefined;
if (!mime || !mime.startsWith('image/')) {
return reply.code(400).send({ error: 'Only image uploads are allowed' });
}
const saved = await saveUploadedImage(file, 'content', fastify.log);
return reply.code(201).send({ imageUrl: saved.imageUrl });
} catch (err: any) {
if (err?.statusCode === 413) {
return reply.code(413).send({ error: err.message });
}
fastify.log.error({ err }, 'Upload failed');
return reply.code(500).send({ error: 'Failed to upload image' });
}
});
// List all content sections
fastify.get('/content', {
preHandler: [fastify.authenticate],