Commit Graph
18 Commits
Author SHA1 Message Date
Florian Wilde 1cf1df54df Kurzhilfe erklaert das Hinzufuegen einer Domain
Eine Domain zu registrieren genuegt nicht — DNS, Reverse Proxy und
Zertifikat muessen dazu passen. Bislang stand das nur im README, also
genau dort nicht, wo man es braucht.

Auf der Domain-Seite oeffnet ein Knopf die Erklaerung in sechs Schritten,
mit den drei noetigen Kopfzeilen und einem Pruefbefehl. Der Knopf selbst
ist als HelpButton allgemein gehalten, damit andere Verwaltungsseiten
spaeter dieselbe Form bekommen koennen.

Refs #4
2026-08-21 02:36:18 +02:00
Florian Wilde eeaf3820b7 Statische Codes benennen und auf Wunsch ablegen
Das Feld „Name (optional)" stand nur im Dynamisch-Block. Bei einem statischen
Code gab es damit keine Moeglichkeit, ihm einen Namen zum Wiederfinden zu
geben.

Jetzt gilt es in beiden Betriebsarten und tut zweierlei: Es bestimmt den
Dateinamen beim Herunterladen — aus „Plakat Eingang" wird
qr-plakat-eingang.png statt qr-example-com-plakat.png — und es laesst sich
damit ein statischer Code in „Meine Codes" ablegen.

Ein abgelegter statischer Code ist ein Schnappschuss, kein steuerbares Ziel.
Aenderbar ist nur der Name; Inhalt und Gestaltung bleiben, wie sie beim
Ablegen waren, denn genau das steckt im gedruckten Bild. Er bekommt keinen
Kurz-Link, ist unter /r nicht erreichbar und zaehlt keine Scans — in der
Liste steht dort „statisch" und ein Strich. Wer nach dem Ablegen den Inhalt
aendert, bekommt das Ablegen erneut angeboten; der vorherige Eintrag bleibt
als eigener Schnappschuss.

Dafuer traegt QrCode jetzt `mode`, und `slug` darf leer sein. In PostgreSQL
schliessen sich mehrere NULL in einem eindeutigen Index nicht aus, die
Bedingung „je Domain eindeutig" gilt fuer dynamische Codes also unveraendert.

Refs #2
2026-08-20 19:33:49 +02:00
Florian Wilde 46e7eb0b59 Sprechende Kurz-Links: selbst vergeben, aendern, frueher gueltig halten
Schliesst Issue #1. Der Wunsch betraf den Kurz-Link, nicht das Ziel: Er soll
sich von Anfang an bestimmen und spaeter aendern lassen.

Beim Anlegen gibt es dafuer ein Feld, im Bearbeiten-Dialog laesst er sich
aendern. Erlaubt sind Kleinbuchstaben, Ziffern, Bindestriche und „/" als
Trenner — damit sind Adressen wie kunde-a/sommer moeglich. Umlaute bleiben
draussen, weil sie in Adressen unleserlich kodiert werden; Gross- und
Kleinschreibung ist beim Aufrufen egal.

Beim Aendern bleibt die vorherige Adresse gueltig. Das ist der Kern: Auf
gedruckten Codes steht die alte, und genau deren Bestand ist der Sinn eines
dynamischen Codes. Frueher vergebene Adressen stehen im Dialog und lassen
sich einzeln freigeben — fuer den Tippfehler, mit dem noch nichts gedruckt
ist. Solange eine Adresse gehalten wird, kann sie kein anderer Code
uebernehmen; sonst liessen sich fremde gedruckte Codes umlenken.

Vergeben und Aendern ist standardmaessig Administratoren vorbehalten. Ein
Kurz-Link ist eine oeffentliche Adresse, und wer ihn aendert, kann Aushaenge
entwerten. Unter Verwaltung -> Einstellungen laesst sich das fuer Bearbeiter
freigeben; ohne das Recht erscheint das Feld gar nicht, Codes anlegen und
deren Ziel aendern geht weiterhin. Eigene Aktion in der Policy statt Teil von
codes:update, weil sie mehr wiegt.

Technisch: /r und /s sind Sammelrouten, weil ein Kurz-Link „/" enthalten
darf. Die Eindeutigkeit gilt zusaetzlich auf der kleingeschriebenen Form; die
Migration bricht ab, falls der Bestand das nicht zulaesst, statt still Daten
zu verlieren. Der bestehende exakte Index bleibt — er traegt den haeufigen
Fall, den gescannten Code.
2026-08-17 10:41:57 +02:00
Florian Wilde b828f38ca5 Dark Mode: folgt dem System, mit Umschalter
Drei Zustaende in der Kopfzeile — System, Hell, Dunkel. Drei statt zwei, weil
ein reiner Ein/Aus-Schalter nach dem ersten Klick fuer immer eine feste Wahl
bedeutet; wer "System" waehlt, bekommt abends dunkel und morgens hell.

Umgesetzt ueber `light-dark()` im CSS, nicht ueber `dark:`-Klassen. Die
Oberflaeche benutzt an rund 440 Stellen feste Farbklassen; jede davon zu
verdoppeln waere eine riesige Fleissarbeit, und jede kuenftige Zeile muesste
daran denken. Stattdessen entscheiden die Farbvariablen selbst, welchen Wert
sie tragen — gesteuert ueber `color-scheme`. Deshalb genuegt fuer den
Umschalter ein Attribut am html-Element, und die Systemwahl funktioniert
sogar ohne JavaScript. Das Skript im Kopf setzt nur eine ausdrueckliche Wahl,
damit die helle Fassung nicht kurz aufblitzt.

Zwei Faelle liessen sich nicht ueber Variablen loesen: `bg-white` muss im
Dunkeln eine dunkle Flaeche sein, `text-white` dagegen weiss bleiben — beide
teilen sich aber dieselbe Variable. Fuer Flaechen gibt es deshalb jetzt
`bg-surface`. Ebenso brauchen Akzent-Links im Dunkeln eine hellere Stufe als
Akzent-Knoepfe: dafuer `text-accent-text` und `bg-accent-soft`.

Bewusst unveraendert bleiben die QR-Codes selbst — deren Farben legt die
Gestaltung fest.

Die dunklen Werte sind keine Spiegelung: Reines Schwarz unter weisser Schrift
flimmert. Alle Textkontraste sind gemessen und liegen ueber 4,5:1, der
niedrigste bei 5,9:1.
2026-08-16 09:09:10 +02:00
Florian Wilde 4046b72626 Erscheinungsbild im Admin-Bereich anpassbar
Name, Zusatz in der Fusszeile, Akzentfarbe, Logo und Favicon lassen sich
unter Verwaltung -> Erscheinungsbild einstellen. Sie gelten fuer die ganze
Installation und liegen in einer eigenen Tabelle mit genau einer Zeile.

Zur Farbe: Tailwind 4 uebersetzt bg-indigo-600 zu
background-color: var(--color-indigo-600). Die Anwendung schreibt diese
Variablen zur Laufzeit neu — damit folgen alle rund 60 eingefaerbten Stellen
der gewaehlten Farbe, ohne dass eine einzige Klasse angefasst werden muss.

Uebernommen werden nur Farbton und Saettigung, nicht die Helligkeit. Die
Helligkeitskurve stammt aus der Tailwind-Vorlage. Sonst wuerde ein helles
Gelb die Kopfzeile unlesbar machen, denn auf Stufe 600 steht weisse Schrift.
Die Umrechnung laeuft ueber OKLCH und trifft Tailwinds eigenes Indigo exakt.

Logo und Favicon sind Uploads mit eigener Groessengrenze (2 MB bzw. 512 KB)
und eigener Rechtepruefung: instance:manage statt codes:create. Uploads haben
jetzt ein Feld "purpose" — sonst haette die Aufraeumroutine fuer herrenlose
Dateien Logo und Favicon nach einem Tag geloescht. Ein ersetztes Bild wird
gelaescht, und eine Datei aus einem QR-Code laesst sich nicht als Logo
einhaengen.

Auf den Landingpages gelten Farbe und Favicon, aber weder Logo noch
Navigation — wer scannt, soll die Seite des Inhalts sehen.

Ein erster Entwurf hielt das Erscheinungsbild 15 Sekunden im Speicher, um die
Abfrage je Seitenaufruf zu sparen. Das ging schief: API-Route und Layout
laufen in getrennten Modulinstanzen, das Verwerfen beim Speichern erreichte
die andere Instanz nicht — nach dem Speichern blieb die Oberflaeche
unveraendert. Jetzt ohne Puffer.
2026-08-16 02:18:49 +02:00
Florian Wilde 1064915d37 Startseite: „Beliebt" ersetzt durch „nur dynamisch"
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.
2026-08-15 07:39:20 +02:00
Florian Wilde e296b1e6c2 Phase C: die letzten sechs Typen — PDF, Bild, Feedback und Datei-Upload
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.
2026-08-14 23:34:28 +02:00
Florian Wilde fdf6452b35 Typen Social Media und Video
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.
2026-08-14 22:44:23 +02:00
Florian Wilde e627f7c372 Landingpages als eigene Gattung, erster Typ: Mehrere Links
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.
2026-08-14 22:27:49 +02:00
Florian Wilde b3d386b0d2 Meine Codes: Listenansicht mit Suche, Sortierung und Gruppenfilter
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.
2026-08-14 15:35:05 +02:00
Florian Wilde 6b2baa66c5 Passwoerter verdeckt, Kontobereich mit eigenem Passwortwechsel
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.
2026-08-14 13:46:21 +02:00
Florian Wilde 2a80f15e20 Phase B2: mehrere Domains fuer Kurz-URLs
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.
2026-08-14 13:10:12 +02:00
Florian Wilde f1cdb3da13 Phase B1: Anmeldung, Benutzer, Gruppen und Rollen
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.
2026-08-14 12:31:16 +02:00
Florian Wilde 7f7cb6fa20 Phase A: acht weitere QR-Typen
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.
2026-08-14 11:30:09 +02:00
Florian Wilde c6da20f419 Verwaltung: QR-Typen einzeln freigeben oder ausblenden
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).
2026-08-14 10:48:21 +02:00
Florian Wilde 4c8adf3f4a Vorschau-Layout und Hintergrund der Vorschauflaeche verbessert
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.
2026-08-13 16:59:49 +02:00
Florian Wilde 5806dc4ce0 Docker-Stack, README und Korrekturen aus dem ersten Durchlauf
- 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.
2026-08-13 16:48:28 +02:00
Florian Wilde ad4cd92f8e V1: Next.js-App mit QR-Typ-Registry, Website-Generator und dynamischen Codes
- 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.
2026-08-13 15:54:47 +02:00