Der Upload lief gegen einen einzigen globalen Wert von 5 MB, der fuer alle
Dateitypen galt. Eine Getraenkekarte liegt aber schnell bei 15-20 MB, das
Hochladen scheiterte deshalb zuverlaessig.
- MAX_FILE_SIZE (Bilder) von 5 auf 20 MB. Bilder werden ohnehin auf 1600px
heruntergerechnet, die Grenze muss nur ein unbearbeitetes Handyfoto
durchlassen - 5 MB reichten dafuer schon nicht.
- MAX_PDF_SIZE neu, 40 MB. PDFs werden unveraendert abgelegt.
- Beides ueber Umgebungsvariablen uebersteuerbar.
Die Registrierung von multipart nimmt den groesseren der beiden Werte als
Obergrenze, die Routen setzen ihn per request.file({ limits }) auf ihren
eigenen herunter. Die Fehlermeldung im PDF-Zweig nannte bisher die
Bildgrenze.
Geprueft am laufenden Container, auch mit --memory=512m wie auf Fly: 18-MB-
PDF und 18-MB-Foto gehen durch, 45 MB bzw. 25 MB werden mit 413 und
passender Meldung abgelehnt, kein OOM.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Astro scoped <style>-Bloecke und schraenkt jede Regel auf
[data-astro-cid-...] ein. Dieses Attribut setzt es nur auf Elemente, die zur
Bauzeit im Template stehen. Die Karten der Event-, Gallery- und
Banner-Listen entstehen aber erst zur Laufzeit per innerHTML und tragen es
nicht - fuer sie hat kein einziger Selektor gegriffen.
Betroffen waren .card, .thumb, .muted, .row-buttons, .pill, .drag-handle und
.highlight-row. Sichtbar wurde es an den Vorschaubildern: sie liefen in
voller Aufloesung ueber die Spalten hinaus. Auch die urspruengliche Regel
max-width:100% hatte deshalb nie gewirkt, nicht erst die neue feste Box.
Behoben mit is:global. Die Seite ist ein eigenstaendiges Dokument mit
eigenem <html>, es kann also nichts anderes davon betroffen sein.
Der erklaerende Kommentar steht als {/* */} im Template und landet damit
nicht in der ausgelieferten Seite.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Bilder, die vor der Umstellung hochgeladen wurden, liegen unverkleinert im
Originalformat im Repo - damals war sharp im Container kaputt, es gab also
weder Verkleinerung noch Formatwandlung. Das Skript schickt sie durch
dieselbe Verarbeitung wie einen frischen Upload.
node dist/scripts/convert-images-to-avif.js # nur anzeigen
node dist/scripts/convert-images-to-avif.js --apply # wirklich tun
Ohne --apply wird nichts geschrieben.
Ablauf: Workspace auf den Repo-Stand bringen, jedes referenzierte Bild
umwandeln und nach seinem Inhalt benennen (Event-Titel, Alt-Text, Zweck),
die Verweise in Events, Gallery und Textbereichen nachziehen, die
Vorgaenger wegraeumen und committen.
Weggeraeumt wird nur, was das CMS selbst angelegt hat. Ein handgepflegtes
event_karaoke.jpg bekommt zwar eine AVIF-Fassung und der Verweis zeigt
darauf, das Original bleibt aber liegen und wird im Bericht genannt.
saveImageBuffer aus upload.service.ts herausgeloest, damit Upload und
Skript nachweislich dieselbe Verarbeitung benutzen statt zweier Kopien.
replaceImageUrls ersetzt Pfade in den Textbereichen, ohne die restliche
JSON-Struktur anzufassen.
Durchgespielt an vier Bildern zu je ~656 KB: 2.56 MB -> 0.16 MB. Die
Vorschau schreibt nichts, ein zweiter Lauf findet nichts mehr, die
Highlights-Liste bleibt unversehrt und alle Bilder werden danach als
image/avif ausgeliefert.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Uploads werden nach AVIF gewandelt statt nach WebP und heissen jetzt nach
dem Inhalt statt nach Zeitstempel und Zufall:
- Events uebernehmen den Event-Titel, die Gallery den Alt-Text, Bilder in
den Textbereichen ihren Zweck, PDFs den urspruenglichen Dateinamen.
- Ohne solche Angabe wird der Dateiname vor dem Upload verwendet.
- Umlaute werden ausgeschrieben (Getraenkekarte, nicht Getrnkekarte),
Sonderzeichen und Pfadanteile fallen weg.
- Gleiche Namen werden durchnummeriert, damit zwei Events namens
"Karaoke" sich nicht gegenseitig ueberschreiben.
Besitz statt Namensmuster:
Die Loeschsicherheit haing bisher am Namensmuster <zeitstempel>-<zufall>.
Mit sprechenden Namen traegt der Name diese Information nicht mehr - ein
hochgeladenes karaoke-abend.avif ist von einem handgepflegten
event_karaoke.jpg nicht zu unterscheiden. Deshalb fuehrt das CMS jetzt in
managed_assets Buch darueber, welche Dateien es selbst angelegt hat, und
loescht ausschliesslich diese. Uploads von vor der Umstellung werden
weiterhin am alten Muster erkannt, damit sie aufraeumbar bleiben.
initDatabase() legte Tabellen nur an, wenn users noch fehlte. Auf einer
bestehenden Datenbank waere managed_assets damit nie entstanden. Der
Block laeuft jetzt bei jedem Start; alle Anweisungen sind IF NOT EXISTS.
Nebenbei repariert: die Formulare schickten den Alt-Text NACH der Datei.
Zu dem Zeitpunkt hat der Server ihn noch nicht geparst, in der Gallery
landete deshalb immer der Dateiname als Alt-Text. Textfelder gehen jetzt
vor der Datei raus.
Gemessen an einem 4032x3024-Bild: 0.90 MB rein, 36 KB AVIF bei 1600x1200
raus, 579 ms. Das Testbild ist synthetisch und komprimiert besser als ein
echtes Foto - die Verkleinerung auf 1600px greift aber immer.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Nur bauen was sich geaendert hat:
- deploy_backend laeuft nur bei Aenderungen unter backend/, deploy_frontend
nur bei src/, public/ und den Frontend-Konfigdateien. Die haeufigsten
Commits sind Inhaltsaenderungen aus dem CMS und fassen nur src/ und
public/ an - die brauchen kein Backend-Deployment mehr.
- Der Abhaengigkeits-Audit laeuft nur noch, wenn sich package.json oder
package-lock.json aendern. Sonst kam bei jedem Inhalts-Commit derselbe
Bericht nach Discord.
- "[ALL]" in der Commit-Message erzwingt weiterhin den vollen Durchlauf.
Build-Kontext:
- Es gab kein .dockerignore im Root. Bei jedem Frontend-Deploy wanderte
das komplette Repo zum Fly-Remote-Builder, inklusive 50 MB Git-Historie
und des gesamten Backends. 98 MB -> 47 MB.
- Zwei tote Zeilen im Frontend-Dockerfile entfernt: ein Root-styles/ gibt
es nicht und /styles/ wird als URL nirgends verwendet.
Backend-Image:
- npm ci lief in beiden Stages, better-sqlite3 wurde also doppelt
uebersetzt. Jetzt einmal im Builder, danach npm prune --omit=dev und die
fertigen node_modules wandern weiter.
- Die Build-Werkzeuge sind raus aus der Laufzeit-Stage. Das apk del vorher
hat sie nur unsichtbar gemacht, die Layer blieben im Image.
- Ohne Cache lokal: 1:31 -> 0:29. Image: 845 MB -> 312 MB.
sharp war die ganze Zeit kaputt:
- backend/package-lock.json enthielt als einziges Plattform-Binary
@img/sharp-win32-x64, das Lockfile stammt von einer Windows-Maschine.
npm ci installiert strikt nach Lockfile, auf Alpine kam damit gar kein
sharp-Binary an. Der Upload fiel jedes Mal auf den Fallback zurueck:
keine Verkleinerung auf 1600px, keine WebP-Wandlung, das Originalbild
landete unveraendert im Repo. Genau deshalb sind die Bilder so gross -
in der Historie liegen entsprechend .jpeg statt .webp.
- Lockfile mit allen Plattformvarianten neu aufgeloest. Keine einzige
bestehende Paketversion aendert sich dabei; dazugekommen sind die
Binaries, weggefallen sind 12 Postgres-Pakete, die drizzle-kit optional
mitzieht und die hier niemand benutzt.
- vips aus dem Image entfernt, sharp bringt seit 0.33 sein eigenes libvips
mit.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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]>
Die Bild-URLs in der DB sind /images/events/... bzw. /images/gallery/... -
das ist der Pfad auf der publizierten Astro-Seite. Das Backend lieferte die
Dateien aber nur unter /static/ mit Root auf dem Workspace aus, die Datei lag
also unter /static/public/images/... Ein Aufruf von /images/... traf auf gar
keine Route, kam als JSON-404 zurueck und wurde vom Browser als
OpaqueResponseBlocking verworfen. Dadurch waren im Adminbereich saemtliche
Event- und Gallery-Bilder kaputt.
Zusaetzlicher statischer Mount /images/ -> <workspace>/public/images. Die DB
bleibt unveraendert und die URLs stimmen weiterhin fuer die publizierte Seite.
/static/ bleibt zusaetzlich bestehen.
Beide Verzeichnisse werden vorab angelegt - @fastify/static verweigert sonst
die Registrierung, wenn der Workspace noch nicht geklont ist.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Die Pipeline hatte nur einen Deploy-Step fuer das Frontend (App gallus-pub).
Die Backend-App gallus-cms-backend wurde von der CI nie ausgerollt, sondern
haing am letzten manuellen fly deploy. Ein Push auf main hat damit zwar die
Admin-Seite aktualisiert, aber nie die API dahinter.
Step laeuft vor deploy_frontend und nutzt denselben FLY_API_TOKEN.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Publish schlug bei jedem Versuch mit "spawn git ENOENT" fehl, obwohl git im
Container installiert ist. Node meldet ENOENT auch dann, wenn das cwd des
Kindprozesses nicht existiert: initialize() setzte simple-git auf den
Workspace, loeschte diesen per rm -rf und startete den Clone anschliessend
aus dem geloeschten Verzeichnis heraus.
Nebeneffekt davon: da Uploads unter GIT_WORKSPACE_DIR/public/images liegen,
hat jeder fehlgeschlagene Publish die hochgeladenen Bilder mitgeloescht.
git.service.ts:
- Clone laeuft aus dem Elternverzeichnis statt aus dem geloeschten Ziel
- Re-Clone nur noch wenn kein brauchbares Repo vorhanden ist
- Uploads werden ueber den Re-Clone hinweg gesichert (Repo-Stand gewinnt)
- commitAndPush scheitert nicht mehr, wenn es nichts zu committen gibt
- reset() nur bei echtem Repo, mit Ausnahme fuer public/images
- Token wird in Fehlermeldungen maskiert
Dockerfile:
- COPY von src/db/migrations entfernt. Das Verzeichnis war nie im Git und
liess den Image-Build scheitern. Zur Laufzeit wird es nicht gebraucht,
das Schema legt initDatabase() selbst an.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>