Das Skript brach mit „laesst sich nicht ohne Merge aktualisieren. Bitte von
Hand pruefen" ab — und liess den Betreiber damit allein. Genau dieser Fall
ist auf dem Server aufgetreten, nachdem hier die Historie neu geschrieben
wurde: Alte und neue Commits haben dann keinen gemeinsamen Vorfahren mehr.
Jetzt unterscheidet das Skript die beiden Ursachen und nennt den Ausweg. Bei
neu geschriebener Historie steht der vollstaendige Befehl da, samt dem
Hinweis, dass die .env davon unberuehrt bleibt. Bei eigenen Commits werden
sie aufgelistet.
Beide Zweige an nachgebauten Repositories geprueft.
Die Frage kam auf und die Antwort gehoert festgehalten, sonst stellt sie sich
in einem halben Jahr wieder — dann ohne die Begruendung.
Kurz: Es gibt keine Selbstregistrierung. Konten legt ein Administrator an, der
die Adresse selbst eintraegt; eine Bestaetigungsmail wuerde also niemanden
hereinlassen und niemanden fernhalten. Und weil es keinen Reset per E-Mail
gibt, ist die unbestaetigte Adresse auch kein Uebernahmeweg. Noetig wuerde sie
in dem Moment, in dem sich Leute selbst registrieren duerfen.
Dazu das Zeitfenster der Ersteinrichtung ausdruecklich benannt: Das erste
Konto entsteht ohne Legitimation und ist gleich Administrator. Dagegen hilft
keine Verifizierung, sondern nur die Reihenfolge beim Aufsetzen — oder ein
einmaliges Token, das jetzt auf der Roadmap steht.
Zehn Befunde, alle behoben. Die wichtigsten:
scrypt lief mit N=2^15; das OWASP Password Storage Cheat Sheet nennt 2^17.
Jetzt 2^17, alte Hashes bleiben gueltig und werden beim naechsten Anmelden
still erneuert — das ist der einzige Moment, in dem das Klartextpasswort
vorliegt.
Passwoerter hatten keine Obergrenze. Ein Passwort von mehreren Megabyte haette
scrypt beschaeftigt, und zwar an einem Endpunkt ohne Anmeldung. Jetzt 128
Zeichen, geprueft vor dem Hashen.
Als CSRF-Schutz gab es nur SameSite=Lax. Das Merkblatt sagt ausdruecklich,
dass das allein nicht genuegt — besonders beim Upload, der multipart
entgegennimmt und damit von einem fremden Formular erreichbar waere. Jetzt
wird bei jedem schreibenden Zugriff der Origin gegen den Host der Anfrage
geprueft (nicht gegen BASE_URL, die Anwendung laeuft unter mehreren Domains).
Es gab keinen einzigen Schutzheader — die Verwaltung liess sich einbetten und
per Clickjacking fernsteuern. Jetzt frame-ancestors none, X-Frame-Options,
nosniff, Referrer-Policy und Permissions-Policy; X-Powered-By ist weg.
Sitzungen hatten keine absolute Obergrenze: eine gestohlene Sitzung haette
durch regelmaessige Nutzung unbegrenzt weitergelebt. Jetzt SESSION_MAX_DAYS
neben SESSION_IDLE_DAYS, beide einstellbar.
Dazu: Secure auch hinter einem TLS-Proxy, weniger Auskunft ueber fremde
Konten bei der Anmeldung, eine Bremse je Absender gegen breites
Durchprobieren, Ersteinrichtung ohne Wettlauf, und abgelaufene Sitzungen
werden aufgeraeumt.
Die Datei heisst proxy.ts statt middleware.ts — Next 16 hat die Konvention
umbenannt. /api/uploads bleibt davon ausgenommen: Der Proxy puffert
Anfragekoerper nur bis 10 MB, eine Datei an der Groessengrenze kaeme sonst
unvollstaendig an. Die Herkunftspruefung macht die Route dort selbst.
Befund, Begruendungen und was bewusst offen bleibt: docs/sicherheit.md
Das Abzeichen stammte aus Version 1, als genau ein Typ nutzbar war. Seit alle
15 gebaut sind, stand es an jeder Kachel und sagte damit nichts.
An seiner Stelle steht jetzt nur noch dort etwas, wo es die Wahl beeinflusst:
sechs Typen brauchen zwingend eine Kurz-URL und funktionieren deshalb nicht
ohne diesen Server. Die uebrigen neun Kacheln bleiben unbeschriftet.
Die Badge-Variante hiess "popular" und wird an drei weiteren Stellen als
Hervorhebung benutzt — sie heisst jetzt "accent", weil sie mit Beliebtheit
nichts zu tun hat.
Dazu die Roadmap neu geordnet: Typen je Gruppe, Statistik, Papierkorb, Logo
im Code, Ablaufdatum — in dieser Reihenfolge, jeweils mit dem, was dabei zu
entscheiden ist.
Kurz im README: was die Anwendung kann, auf einen Blick. Lang in
docs/funktionen.md: jeder Typ mit seinen Grenzen, dazu Landingpages, Dateien,
Feedback, Rechte, Single Sign-on, Domains und Betrieb — samt der Frage, warum
manches so entschieden ist.
docs/roadmap.md haelt fest, was in welcher Reihenfolge gebaut wurde, und
sammelt Vorschlaege fuer danach. Bewusst als Vorschlaege mit grober
Aufwandsschaetzung, nicht als Zusage: was zuerst kommt, entscheidet, was im
Betrieb fehlt.
Alle 15 Typen sind gebaut — der Abschnitt beschrieb noch die Planung. Statt
dessen: wie ein weiterer Typ dazukommt, und was als Naechstes sinnvoll waere.
docs/anleitung.md fuehrt vom ersten Code bis zur Auswertung von
Rueckmeldungen: statisch gegen dynamisch, Gestaltung, Meine Codes, die fuenf
Typen mit eigener Seite, Verwaltung, Konto und haeufige Fragen.
Die Bilder erzeugt docs/screenshots.mjs. Von Hand aufgenommene Bilder
veralten beim ersten Umbau — so sind sie in einer Minute neu erzeugt, samt
Beispieldaten, die das Skript hinterher wieder entfernt.
Anmeldefluss mit PKCE (S256), State und Nonce. Das ID-Token wird gegen den
JWKS des Anbieters geprueft — Signatur, Aussteller, Empfaenger, Ablauf und
Nonce; alles Noetige kommt aus dem Discovery-Dokument, damit die Anbindung
nicht auf Authentik festgelegt ist.
Wiedererkannt wird jemand an der Kennung des Anbieters (sub), nicht an der
E-Mail-Adresse — Adressen aendern sich. Ein bestehendes Konto mit derselben
Adresse wird verknuepft statt verdoppelt, aber nur bei bestaetigter Adresse:
sonst koennte sich beim Anbieter jemand eine fremde Adresse eintragen und
damit ein hiesiges Konto uebernehmen.
Uebernommen wird bewusst nur, wer jemand ist — plus die Instanz-Administration,
wenn OIDC_ADMIN_GROUP gesetzt ist. Gruppen und Rollen bleiben Sache dieser
Anwendung: die Gruppen im Verzeichnis bedeuten etwas anderes als die hier.
Konten aus dem Verzeichnis haben kein Passwort mehr (passwordHash ist jetzt
optional); Konto-Bereich und Passwort-Anmeldung sagen das statt an einer
leeren Pruefung zu scheitern.
Anleitung mit allen Feldern in Authentik: docs/authentik.md
Damit sind alle 15 QR-Typen gebaut.
PDF und Bild brauchen eine hochgeladene Datei. Die liegt in der Datenbank,
nicht auf der Platte: damit bleibt es bei einem Volumen, das gesichert werden
muss — ein zweites wird beim Sichern zu leicht vergessen. Geprueft wird der
Dateiinhalt, nicht die Endung; eine Textdatei namens harmlos.pdf kommt nicht
durch. SVG ist bewusst nicht erlaubt, weil es Skripte enthalten darf und von
derselben Adresse wie die Anwendung ausgeliefert wuerde. Ausgeliefert wird mit
nosniff und einer CSP-Sandbox, damit auch ein PDF mit Skripten nichts anrichtet.
Dateien haengen am Code: wird er geloescht, geht sie mit. Eine ausgetauschte
oder nie gespeicherte Datei wird nach 24 Stunden weggeraeumt. Eine fremde
Datei laesst sich nicht an den eigenen Code haengen — sonst koennte man sie
einem anderen Code entziehen.
Feedback sammelt Note und Kommentar, ohne Name, IP oder Geraetekennung. Die
Auswertung mit Durchschnitt, Verteilung und Kommentaren ist aus der Codeliste
erreichbar — ein Feedback-Code ohne Auswertung waere nur die halbe Sache.
Social Media buendelt Profile auf einer Seite. Wer „@firma" eintippt, meint
das Profil beim gewaehlten Netzwerk — daraus wird die volle Adresse. Statt
nachgezeichneter Markenlogos steht ein Kreis in der Hausfarbe mit Monogramm;
ein schlecht nachgebautes Logo sieht schlechter aus als ein sauberes Zeichen.
Video erkennt YouTube, Vimeo und direkte Dateien. Der Rahmen zum Anbieter
laedt erst auf Klick, und davor steht, dass dabei Daten uebertragen werden —
diese Seite bekommen Fremde nach einem Scan zu sehen, ungefragt einen
Drittanbieter einzubinden waere nicht in Ordnung. Videodateien laden mit
preload="none": wer unterwegs scannt, zahlt das Volumen.
Die sechs noch offenen Typen haben kein fremdes Ziel, auf das man
weiterleiten koennte — sie brauchen eine Seite. Dafuer bekommt der
Typ-Vertrag `hasLandingPage`: /r/[slug] schickt solche Codes auf /s/[slug],
statt `redirectTarget` zu befragen.
Die oeffentlichen Seiten duerfen die Navigation der Anwendung nicht zeigen —
wer einen Code scannt, hat mit „Verwaltung" nichts zu tun. Deshalb liegt das
App-Chrome jetzt in der Routengruppe (app); das Wurzel-Layout traegt nur noch
html und body. Die URLs aendern sich dadurch nicht.
Bei vielen Projekten ist die Kartenansicht unuebersichtlich. Die Liste ist
jetzt die Voreinstellung, die Karten bleiben ueber einen Schalter erreichbar;
die Wahl merkt sich der Browser.
Dazu Suche ueber Name, Ziel, Kurz-URL und Typ, Sortierung nach Datum, Name
oder Scans sowie ein Gruppenfilter, sobald Codes in mehr als einer Gruppe
liegen. Die Seite ist breiter als die uebrigen, damit die Namen Platz haben.
Die Tabelle hat feste Spaltenbreiten: mit automatischem Layout hat ihre
Eigenbreite die ganze Seite seitlich scrollbar gemacht, auch wenn der
Container sie eigentlich beschnitten hat. Auf schmalen Schirmen weichen die
hinteren Spalten, die Kurz-URL rueckt unter den Namen.
Der Schalter blieb wirkungslos: AUTH_ENABLED stand zwar in der .env und im
README, fehlte aber im environment-Block des app-Dienstes. Docker Compose
liest die .env nur, um Platzhalter wie \ zu ersetzen - in den
Container gelangt ausschliesslich, was unter environment: aufgefuehrt ist.
Wer die Anmeldung einschalten wollte, blieb deshalb ohne jede Rueckmeldung
im Zustand 'ohne Anmeldung' haengen.
Gefunden hat das Florian beim Benutzen. Meine Pruefungen liefen alle gegen
'next start' mit direkt gesetzter Umgebungsvariable und konnten die Luecke
zwischen .env und Container deshalb gar nicht sehen.
- AUTH_ENABLED steht jetzt im environment des app-Dienstes, mit Hinweis im
docker-compose.yml, warum das Ergaenzen dort nicht vergessen werden darf.
- Neue Pruefung compose-env.mjs: vergleicht die dokumentierten Variablen mit
dem aufgeloesten Compose-Setup und mit der Umgebung des laufenden
Containers. Genau diese Klasse von Fehler faellt damit kuenftig auf.
- README: Hinweis, dass die .env von git pull nicht angefasst wird und die
Zeile in aelteren Installationen fehlt, plus ein Befehl zum Nachpruefen.
Geprueft gegen den Container in beiden Stellungen: 63 Pruefungen fuer
Anmeldung und Rollen sowie 34 fuer Multi-Domain mit AUTH_ENABLED=true,
dazu 293 mit false.
Zwei berechtigte Rueckmeldungen von Florian:
1. In der Benutzerverwaltung standen die Passwortfelder auf type='text',
das Passwort war also im Klartext lesbar - vor fremden Augen und in
jeder Bildschirmuebertragung. Alle Passwortfelder nutzen jetzt ein
gemeinsames PasswordInput: verdeckt als Voreinstellung, mit Auge zum
Aufdecken. Weil ein Administrator das Passwort weitergeben muss, gibt es
daneben einen Generator fuer ein zufaelliges, vorlesbares Passwort in
Vierergruppen.
2. Es gab keinen Kontobereich. Neu ist /konto mit Profil, den eigenen
Gruppen samt Rollenerklaerung, Passwortwechsel und Abmelden. Im
Kopfbereich fuehrt ein Namenskuerzel dorthin. Ist die Anmeldung
ausgeschaltet, steht dort 'Ohne Anmeldung' und die Seite erklaert, wie
man sie einschaltet - vorher suchte man den Abmelden-Knopf vergeblich.
Dabei eine echte Luecke geschlossen: ein Benutzer konnte sein Passwort gar
nicht selbst aendern, nur ein Administrator konnte es setzen. Der Wechsel
verlangt das bisherige Passwort, lehnt ein identisches ab und beendet alle
anderen Sitzungen - die eigene bleibt bestehen.
Nebenbei: die Betriebsart-Knoepfe im Generator haben jetzt data-mode. Ueber
aria-pressed waren sie nicht mehr eindeutig, seit das Passwortfeld einen
eigenen Umschalter hat.
Geprueft: 63 Pruefungen fuer Anmeldung inkl. Passwortwechsel und Konto,
dazu 280 bestehende bei ausgeschalteter Anmeldung.
Eine Domain kann mehreren Gruppen dienen (n:m ueber DomainGrant), und der
Slug eines Codes ist nur noch je Domain eindeutig - derselbe sprechende
Slug darf also auf zwei Domains liegen.
Sicherheitsrelevanter Kern: die Weiterleitung liest den Hostnamen aus
X-Forwarded-Host bzw. Host und prueft ihn gegen die Domain-Tabelle. Ein
unbekannter Hostname endet mit 404, faellt also auf die Standard-Domain
zurueck und findet dort nichts. Ueber einen gefaelschten Header laesst sich
damit keine fremde Domain ansprechen. Auch der Hostname fuer die Vorschau
kommt aus der Datenbank, nicht aus der Anfrage.
- Migration legt die Spalte erst nullable an, fuellt sie mit der
Standard-Domain und macht sie danach verpflichtend; der globale
Slug-Index weicht dem zusammengesetzten aus (domainId, slug).
- Verwaltung -> Domains: registrieren, Gruppen zuteilen, Voreinstellung je
Gruppe, abschalten. Domains mit Codes lassen sich weder loeschen noch
einer Gruppe entziehen, die noch Codes darunter hat - beides wuerde
gedruckte Codes ins Leere laufen lassen.
Ein Fehler dabei gefunden und behoben: die Migration kennt BASE_URL nicht
und legt die Standard-Domain mit einem Platzhalter-Hostnamen an. Korrigiert
wurde er bisher nur beim Oeffnen der Domain-Verwaltung - auf einer frischen
Installation waeren also Kurz-URLs auf 'standard.invalid' entstanden.
ensureDefaultDomain() laeuft jetzt vor jeder Verwendung.
Geprueft: 34 neue Pruefungen fuer Host-Aufloesung, Spoofing, Slug je Domain
und Zuteilung; dazu 280 bestehende bei ausgeschalteter Anmeldung.
Umschaltbar ueber AUTH_ENABLED in der .env. Steht sie auf false, verhaelt
sich alles wie bisher - geprueft mit den 200 bestehenden Pruefungen.
Kern ist eine Actor- und Policy-Schicht: getActor() liefert bei
ausgeschalteter Anmeldung einen system-Actor mit Vollzugriff, sonst den
angemeldeten Benutzer. Dadurch braucht keine einzige Route ein
'if (anmeldungAktiv)' - sie fragen alle nur can() bzw. requirePermission().
- Datenmodell: User, Session, Group, Membership; Codes bekommen Gruppe und
Ersteller. Die Migration legt eine Standardgruppe an und ordnet ihr
bestehende Codes zu, bevor die Spalte verpflichtend wird - sonst
scheiterte sie auf jeder bereits benutzten Installation.
- Passwoerter mit scrypt aus der Node-Standardbibliothek. Bewusst kein
Argon2: das braeuchte ein natives Modul passend zur Container-Plattform.
- Sitzungen in der Datenbank, im Cookie nur ein Zufallstoken; gespeichert
wird dessen SHA-256. Deaktivieren oder ein neues Passwort beendet alle
offenen Sitzungen sofort.
- Aussperrschutz: der letzte aktive Administrator kann sich nicht selbst
entmachten, deaktivieren oder loeschen. Der Schalter liegt in der .env,
nicht in der Oberflaeche - so bleibt der Weg zurueck immer offen.
- Ersteinrichtung legt den ersten Administrator an und schliesst sich danach.
- Die Weiterleitung /r/{slug} bleibt oeffentlich, sonst liesse sich kein
gedruckter Code mehr scannen.
Zwei Fehler dabei gefunden und behoben:
- 'return handleLogin(...)' innerhalb eines try-Blocks gibt das Promise
zurueck, bevor es sich aufloest. Die Ablehnung kam nie im catch an und
wurde zu einem nackten 500er statt einer 401. Jetzt mit await.
- Die Fehlerklassen wurden ueber instanceof erkannt. type-module.ts landet
aber sowohl im Server- als auch im Client-Buendel, es gibt also zwei
Klassenidentitaeten - saubere 400er wurden dadurch still zu 500ern. Die
Erkennung laeuft jetzt ueber eine Eigenschaft.
Geprueft: 51 neue Pruefungen fuer Anmeldung und die vollstaendige
Rollenmatrix, dazu die 200 bestehenden bei ausgeschalteter Anmeldung.
Acht nummerierte Schritte vom leeren Server bis zum ersten QR-Code, mit
den erwarteten Ausgaben von 'docker compose ps' und den Protokollzeilen,
an denen man erkennt, dass die Migration durchgelaufen ist.
Dazu zwei Abschnitte, die vorher fehlten:
- Betrieb hinter dem Nginx Proxy Manager samt der Felder, die im Proxy Host
einzutragen sind, und dem Hinweis, dass BASE_URL dann die oeffentliche
https-Adresse sein muss.
- Fehlersuche fuer die Faelle, die tatsaechlich vorkommen: fehlende .env,
belegter Port 3000, unhealthy Datenbank, Passwort in DATABASE_URL passt
nicht zu POSTGRES_PASSWORD, und der haeufigste Fehler ueberhaupt - eine
BASE_URL auf localhost, wodurch dynamische Codes auf dem Handy ins Leere
fuehren.
E-Mail, SMS, WhatsApp, WLAN, Standort, vCard, Zahlung und App-Link sind
umgesetzt. Damit sind 9 der 15 Typen nutzbar.
Erweiterungen am Typ-Vertrag, die dafuer noetig waren:
- supportsStatic: nicht jeder Typ kann statisch. Beim App-Link steht das
Ziel erst beim Scannen fest, also gibt es ihn nur dynamisch.
- RedirectContext: die Weiterleitung reicht den User-Agent durch, damit der
App-Link iOS, Android und alles Uebrige unterscheiden kann.
- assertDynamicPossible(): supportsDynamic ist eine Eigenschaft des Typs,
aber nicht jede Eingabe taugt dafuer. Ein GiroCode ist kein Web-Ziel,
obwohl der Typ 'Zahlung' als Zahlungslink sehr wohl dynamisch sein kann.
Ohne diese Probe liess sich ein Code speichern, der erst beim Scannen
aufgefallen waere - also nach dem Drucken. Greift beim Anlegen und beim
Bearbeiten.
Sorgfalt bei den Formaten, gesammelt in src/lib/qr/encoding.ts:
- WLAN maskiert ; , : \\ und setzt rein hexadezimale Werte in
Anfuehrungszeichen, sonst deuten Geraete sie als Hexzahl.
- vCard 3.0 mit CRLF, Maskierung und Faltung nach 75 Oktetten, oktettweise
gezaehlt, damit kein mehrbytiges Zeichen zerrissen wird.
- GiroCode nach EPC069-12 mit fester Feldreihenfolge, IBAN-Pruefsumme nach
Modulo 97 und der 331-Byte-Grenze.
- Rufnummern werden nach E.164 normalisiert, '(0)' faellt weg.
- Standort nimmt Koordinaten auch aus einem Google-Maps- oder Apple-Link an.
Formular-Bausteine (Select, Textarea, Checkbox, FieldRow, FieldSet) fuer die
mehrfeldrigen Typen ergaenzt.
Geprueft: 225 Pruefungen, davon 80 neu - Dekodierung je Typ aus dem fertigen
PNG, Validierung, Betriebsarten, geraeteabhaengige Weiterleitung und die
Formulare im Browser.
Neue Seite Verwaltung -> QR-Typen mit einem Schalter je Typ. Abgeschaltete
Typen verschwinden vollstaendig von der Startseite und lassen sich nicht
mehr neu anlegen.
- Neue Tabelle QrTypeSetting; fehlender Eintrag heisst freigegeben, damit
eine frische Installation ohne Vorbelegung laeuft und spaetere Typen
automatisch erscheinen.
- Registry-Feld 'active' heisst jetzt 'implemented' und beschreibt nur noch,
ob der Typ ausprogrammiert ist. Ob er erscheint, entscheidet zusaetzlich
die Freigabe aus der Datenbank (src/lib/qr/availability.ts).
- Bestehende dynamische Codes bleiben von einer Abschaltung unberuehrt: die
Weiterleitung funktioniert weiter und Bearbeiten bleibt moeglich. Ein
gedruckter Code darf durch einen Schalter nicht ungueltig werden.
- resolveQrRequest ist dadurch asynchron geworden; QrRequestError liegt jetzt
in type-module.ts, sonst gaebe es einen Ringschluss mit availability.ts.
- Startseite meldet jetzt auch den Fall, dass nur noch Platzhalter
freigegeben sind - vorher standen dort 14 tote Kacheln ohne Erklaerung.
- Kopfzeile: Schriftzug auf schmalen Geraeten ausgeblendet, sonst passte die
dritte Navigationsschaltflaeche nicht mehr in die Zeile (7px Ueberlauf).
Das Verzeichnis war nach dem Entfernen der Next.js-Boilerplate leer, und
leere Verzeichnisse legt git nicht an. Lokal existierte es noch, im Klon
nicht — dort brach 'COPY --from=builder /app/public ./public' ab.
update.sh bzw. update.ps1 holen den neuen Stand und bauen den Stack neu:
- Bricht ab, wenn git, docker compose oder die .env fehlen.
- Bricht ab, wenn es lokale Aenderungen gibt — ein "git pull" wuerde dort
im Konflikt enden.
- Holt den Stand mit "git merge --ff-only" und zeigt die neuen Commits.
- Baut nur neu, wenn es tatsaechlich Aenderungen gab (--force erzwingt es).
- Wartet, bis die Anwendung auf APP_PORT antwortet, und gibt bei Zeitablauf
das Container-Protokoll aus, statt stillschweigend Erfolg zu melden.
- Zeigt zum Schluss die Migrationszeilen aus dem Start und den Status.
- --prune raeumt auf Wunsch verwaiste Images auf, die bei jedem Neubau
zurueckbleiben.
README um einen Abschnitt "Aktualisieren" ergaenzt.
docker/entrypoint.sh muss LF-Zeilenenden behalten. Beim Auschecken auf
einem Rechner mit core.autocrlf=true wuerde daraus CRLF, und der Kernel
faende den Interpreter aus der Shebang-Zeile nicht mehr — der Container
startet dann nicht.
Auf dem Handy stand die Vorschau ganz unten hinter der Gestaltung — man
musste an allen Reglern vorbeiscrollen, um den Code zu sehen. Die
Rasterplaetze werden jetzt ab "lg" ausdruecklich vergeben: auf dem Handy
folgt die Vorschau direkt auf die Eingabe, auf dem Desktop steht sie
weiterhin rechts daneben.
Das Schachbrettmuster hinter der Vorschau entfaellt. Es sollte
Transparenz sichtbar machen, aber jeder erzeugte Code bringt eine
deckende Hintergrundfarbe mit — es war nur Unruhe.
- Dockerfile (4 Stufen) und docker-compose.yml mit Postgres-Healthcheck,
Volume und direktem Port-Mapping 3000:3000, ohne Reverse Proxy.
- Entrypoint wendet beim Start "prisma migrate deploy" an; die Prisma-CLI
bekommt eine eigene Build-Stufe, weil sie mehr als nur @prisma/* braucht.
- prisma.config.ts durch prisma.config.js ersetzt: laedt ohne TypeScript und
ohne dotenv, damit sie im Laufzeit-Image funktioniert.
- Prisma-Client wird erst beim ersten Zugriff erzeugt. Vorher scheiterte
"next build" beim Einsammeln der Routen-Konfiguration ohne DATABASE_URL.
- Postgres-18-Volume auf /var/lib/postgresql gemountet (neues Datenlayout).
- npm 11 blockiert Installationsskripte: erlaubte Pakete stehen jetzt im
Feld "allowScripts" der package.json.
- Lokale Variable "module" ueberall in "qrType" umbenannt (verdeckte sonst
die CommonJS-Variable) und Vorschau-Effekt ohne synchrones setState.
- README.md auf Deutsch: Setup, Konfiguration, Bedienung, Aufbau, API und
Ausblick auf Version 2.
- Erweiterbare Registry aller 15 QR-Typen; nur "Website" ist aktiv, die
uebrigen erscheinen als Platzhalter mit Badge "Bald verfuegbar".
- defineQrType() kapselt Zod-Schema, Kodierung und Redirect-Ziel je Typ,
sodass ein V2-Typ nur aus einem Registry-Eintrag plus Formular besteht.
- Serverseitige QR-Erzeugung (PNG/SVG) mit Farben, Groesse, Fehlerkorrektur
und Rand; Live-Vorschau ueber /api/qr/preview.
- Dynamische Codes: Kurz-URL {BASE_URL}/r/{slug}, 302-Redirect mit
Scan-Zaehler, CRUD unter /api/codes.
- Prisma 7 mit pg-Driver-Adapter und Initial-Migration fuer PostgreSQL.
- Deutsche Oberflaeche mit Tailwind CSS, helles Theme, responsiv.