Zur Wahl stehen zwei Quellen: das Logo der Installation aus dem
Erscheinungsbild oder ein eigenes je Code. Die Groesse ist zwischen 10 und
25 Prozent der Kantenlaenge einstellbar, hinter dem Bild liegt eine Flaeche
in der Hintergrundfarbe.
Mit Logo gilt die Fehlerkorrektur H, unabhaengig von der Auswahl — was das
Bild verdeckt, muss sie ausgleichen. Die Auswahl steht dann fest auf H und
sagt auch warum; heimlich geschieht das nicht. Ab 22 Prozent kommt eine
Warnung dazu.
Gerendert wird weiterhin serverseitig, damit Vorschau und Download
uebereinstimmen: das SVG bekommt ein eingebettetes Bild, das PNG wird mit
sharp zusammengesetzt. sharp lag ohnehin als Abhaengigkeit von Next im
Baum und steht jetzt ausdruecklich in der package.json.
Ein eigenes Logo gehoert zur Gestaltung, nicht zum Inhalt — es muss
trotzdem an den Code gehaengt werden. Sonst raeumt die Aufraeumroutine es
nach einem Tag weg und der Code verliert sein Bild.
Refs #8
Ein statischer Code traegt seinen Inhalt selbst — zweimal derselbe Inhalt
ergibt zweimal exakt dasselbe Bild. Bisher entstand der zweite Eintrag
stillschweigend.
Das Anlegen antwortet nun mit 409 und nennt den vorhandenen Eintrag samt
Namen und Datum; die Oberflaeche zeigt das als Warnung mit dem Knopf
"Trotzdem ablegen". Eine Rueckfrage, keine Sperre — zwei Fassungen
desselben Codes koennen gewollt sein.
Verglichen wird Typ und Inhalt innerhalb der Gruppe, nicht die Gestaltung.
Dynamische Codes bleiben aussen vor: zwei Kurz-Links auf dasselbe Ziel
sind der Normalfall, sobald getrennt gezaehlt werden soll.
ApiError traegt dafuer jetzt Status und Antwortkoerper — eine gemeldete
Dublette ist keine Sackgasse, sondern eine Rueckfrage.
Refs #6
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
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.
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.
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.
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.
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.