Files
qrcode-generator/docs/funktionen.md
T
Florian Wilde cdda7df17a Version 1.7.0: Doku durchgesehen und Roadmap angeglichen
Sechs Erweiterungen sind zusammengekommen — Logo im Code, Freigabe je
Gruppe, Domain-Check, Dublettenpruefung, Filter nach Art und die
Kurzhilfe zu den Domains. Dafuer eine neue Minor-Version.

Zwei Stellen waren schlicht falsch und wiegen schwerer als jede Luecke:
Anleitung und Funktionsuebersicht behaupteten beide noch, statische Codes
wuerden "nicht gespeichert" — ueberholt seit 1.5.0. Ebenso die Aussage,
nur "Website" sei ausprogrammiert; das gilt seit 1.1.0 fuer alle fuenfzehn.

Danach die Luecken: Jedes der sechs Features steht jetzt in allen drei
Dokumenten, die es betrifft. Die Bedienungsanleitung hatte keines davon.

Die Roadmap verliert die zwei gebauten Punkte und bekommt einen neuen aus
der Umsetzung: Die Gestaltung eines gespeicherten Codes laesst sich
ueberhaupt nicht aendern, ein Logo also nur ueber die Schnittstelle
nachruesten.

Die Kurzhilfe verwies unter "Nachpruefen" noch auf curl, obwohl seit #7
der Pruefen-Knopf danebensteht.
2026-08-21 11:27:15 +02:00

29 KiB
Raw Blame History

Alle Funktionen im Detail

Die lange Fassung der Übersicht aus dem README. Sie beschreibt, was die Anwendung kann, wo die Grenzen liegen und warum manches so entschieden ist. Wie man es bedient, steht in der Bedienungsanleitung.


Inhalt


QR-Typen

Alle 15 vorgesehenen Typen sind gebaut.

Typ Was hineinkommt Statisch Dynamisch Eigene Seite
Website eine Adresse ✓ ✓ —
vCard Name, Firma, Telefon, E-Mail, Anschrift, Web ✓ — —
App-Link je eine Adresse für iOS, Android und Ausweichfall — ✓ —
Mehrere Links Überschrift, Text, bis zu 20 beschriftete Links — ✓ ✓
Social Media Überschrift, Text, bis zu 12 Profile aus 15 Netzwerken — ✓ ✓
WhatsApp Rufnummer und vorbereitete Nachricht ✓ ✓ —
PDF eine hochgeladene Datei, Titel, Text — ✓ ✓
Bild bis zu 10 hochgeladene Bilder mit Bildunterschriften — ✓ ✓
Video YouTube, Vimeo oder eine direkte Videodatei ✓ ✓ ✓
E-Mail Empfänger, Betreff, Text ✓ ✓ —
SMS Rufnummer und Text ✓ ✓ —
Standort Koordinaten oder Adresse ✓ ✓ —
Zahlung GiroCode (EPC069-12) oder Zahlungslink ✓ nur Link —
WLAN Netzwerkname, Passwort, Verschlüsselung, versteckt ✓ — —
Feedback Frage, Skala, Textfeld, Dankestext — ✓ ✓

Warum manche Typen nur eine Betriebsart können

  • vCard und WLAN wertet das Gerät direkt aus — die Kamera legt einen Kontakt an oder richtet das Netz ein. Über eine Kurz-URL ginge stattdessen nur ein Browser auf.
  • App-Link kann nicht statisch sein: Welches Ziel richtig ist, steht erst beim Scannen fest. Die Weiterleitung wertet den User-Agent aus — iOS in den App Store, Android zu Google Play, alles Übrige auf die Ausweichadresse. (iPadOS meldet sich seit Version 13 als Macintosh und landet deshalb mitunter auf dem Desktop-Weg; für einen Store-Link ist das hinnehmbar.)
  • GiroCode ist kein Web-Ziel, sondern ein Datensatz für Banking-Apps. Als dynamischer Code funktioniert nur die Variante „Zahlungslink“. Die Anwendung weist das beim Speichern und beim Bearbeiten ab, statt einen Code entstehen zu lassen, der erst beim Scannen auffällt — also dann, wenn er schon gedruckt ist.
  • Die fünf Typen mit eigener Seite brauchen eine Seite, die die Anwendung ausliefert. Eine Linkliste oder ein Formular passt nicht in einen einzelnen Code. Video ist die Ausnahme: statisch steht dort die Videoadresse selbst.

Besonderheiten einzelner Typen

Social Media. Als Eingabe genügt das Kürzel — aus @firma wird beim Speichern https://instagram.com/firma. Eine vollständige Adresse bleibt unverändert, damit auch abweichende Profil-Adressen möglich sind. Jedes Netzwerk kann nur einmal vorkommen. Der Katalog umfasst Instagram, Facebook, LinkedIn, Xing, YouTube, TikTok, X, Threads, Mastodon, WhatsApp, Telegram, Pinterest, Snapchat, GitHub und „Website“.

Video. Erkannt werden youtube.com/watch, youtu.be, /shorts/, /embed/, /live/, Vimeo-Adressen sowie die Endungen .mp4, .webm, .ogv, .ogg, .mov und .m4v. Alles Übrige erscheint als schlichter Link — ehrlicher als ein eingebetteter Rahmen, den der Anbieter nicht erlaubt.

WLAN. Das Passwort steht im Klartext im Code; wer ihn scannt oder abfotografiert, kennt es. Für Gästenetze ist das gewollt. Besteht Netzwerkname oder Passwort nur aus Hex-Ziffern, setzt die Anwendung den Wert wie in der Spezifikation vorgesehen in Anführungszeichen — sonst deuten manche Geräte ihn als Hexzahl.

Standort. Statisch entsteht ein geo:-Link, den Karten-Apps direkt öffnen. Dynamisch führt der Weg über OpenStreetMap, weil eine Kurz-URL im Browser landet.


Statisch und dynamisch

Statisch Dynamisch
Im Code steht die Zieladresse selbst {BASE_URL}/r/{slug}
Ziel später änderbar nein ja, jederzeit
Scans gezählt nein ja, mit Datum des letzten
Braucht diesen Server nie dauerhaft
In „Meine Codes“ auf Wunsch, als Schnappschuss immer

Ein statischer Code mit Namen

Beide Betriebsarten haben ein Feld Name (optional). Es tut zweierlei:

  • Es bestimmt den Dateinamen beim Herunterladen — aus „Plakat Eingang“ wird qr-plakat-eingang.png statt qr-example-com-plakat.png.
  • Auf Wunsch lässt sich der Code damit in „Meine Codes“ ablegen, um ihn wiederzufinden und erneut herunterzuladen.

Ein abgelegter statischer Code ist ein Schnappschuss, kein steuerbares Ziel. Änderbar ist nur sein Name; Inhalt und Gestaltung bleiben, wie sie beim Ablegen waren — denn genau das steckt im gedruckten Bild. Er hat keinen Kurz-Link, ist unter /r/… nicht erreichbar und zählt keine Scans; in der Liste steht dort „statisch“ und ein Strich.

Wer nach dem Ablegen den Inhalt ändert, bekommt das Ablegen erneut angeboten — der vorherige Eintrag bleibt als eigener Schnappschuss bestehen.

Dublettenprüfung. Liegt in derselben Gruppe schon ein statischer Code mit identischem Inhalt, hält das Ablegen an und zeigt, welcher Eintrag das ist und wann er entstand. Der Knopf heißt dann Trotzdem ablegen — eine Warnung, keine Sperre: Zwei Fassungen desselben Codes können gewollt sein.

Verglichen wird nur der Inhalt, nicht die Gestaltung. Derselbe Code in Schwarz-Weiß und in Hausfarben gilt also als Dublette — was er streng genommen auch ist, denn gescannt wird beides Mal dasselbe. Dynamische Codes bleiben außen vor: Zwei Kurz-Links auf dasselbe Ziel sind der Normalfall, sobald zwei Kampagnen getrennt gezählt werden sollen.

Ein Logo in der Mitte

Ein Bild in die Mitte des Codes setzen, wie man es von Firmen-Codes kennt. Zur Wahl stehen zwei Quellen:

  • das Logo dieser Installation aus dem Erscheinungsbild — ein Haken genügt, kein Upload je Code;
  • ein eigenes Logo je Code, für Kundenprojekte mit fremdem Zeichen.

Die Größe ist zwischen 10 und 25 % der Kantenlänge einstellbar; voreingestellt sind 20 %, das entspricht rund 4 % der Fläche. Hinter dem Bild liegt eine Fläche in der Hintergrundfarbe, damit es sich absetzt.

Die Fehlerkorrektur wird dabei auf H erzwungen — was das Bild verdeckt, muss sie ausgleichen. Die Auswahl steht dann fest auf H und sagt auch warum; heimlich geschieht das nicht. Ab 22 % erscheint eine Warnung: Die Fehlerkorrektur trägt das zwar, aber manche Scanner-Apps suchen in der Mitte nach Ausrichtungsmustern.

Jeder erzeugte Code wird in der Prüfung wieder eingelesen — über mehrere Größen, Prozentsätze und Inhaltslängen. Das ersetzt trotzdem nicht den einen Scan mit dem eigenen Handy, bevor etwas in den Druck geht.

Ohne eigene Angabe entsteht er aus acht Zeichen eines Alphabets ohne verwechselbare Zeichen (kein 0, O, 1, l, I) — Kurz-URLs werden gelegentlich abgetippt.

Selbst bestimmen lässt er sich beim Anlegen und später ändern:

Erlaubt Kleinbuchstaben, Ziffern, Bindestriche, / als Trenner
Beispiele sommeraktion, tisch-14, kunde-a/flyer
Nicht erlaubt Umlaute, Leerzeichen, Unterstriche, Bindestrich am Rand eines Abschnitts
Länge bis 100 Zeichen, höchstens 3 Abschnitte à 48 Zeichen

Groß- und Kleinschreibung spielt beim Aufrufen keine Rolle: SommerAktion findet sommeraktion. Gespeichert wird kleingeschrieben, und zwei Kurz-Links dürfen sich nicht nur in der Schreibweise unterscheiden.

Beim Ändern bleibt die vorherige Adresse gültig. Das ist der Sinn der Sache: Auf gedruckten Codes steht die alte, und die soll weiter funktionieren. Frühere Adressen stehen im Bearbeiten-Dialog und lassen sich einzeln freigeben — für den Fall eines Tippfehlers, mit dem noch nichts gedruckt ist. Eine freigegebene Adresse ist sofort tot und danach wieder vergebbar. Solange sie gehalten wird, kann sie kein anderer Code übernehmen; sonst ließen sich fremde gedruckte Codes umlenken.

Wer darf das? Standardmäßig nur Administratoren — ein Kurz-Link ist eine öffentliche Adresse, und wer ihn ändert, kann Aushänge entwerten. Unter Verwaltung → Einstellungen lässt sich das für Bearbeiter freigeben. Ohne das Recht erscheint das Feld gar nicht; Codes anlegen und deren Ziel ändern geht weiterhin.

Der Kurz-Link ist je Domain eindeutig, nicht global — derselbe darf auf zwei Domains liegen und dort auf Verschiedenes zeigen.

Die Weiterleitung antwortet mit 302 und Cache-Control: no-store. Ein permanenter Redirect würde in Browsern und Zwischenspeichern hängen bleiben — und genau die Änderbarkeit zunichtemachen, wegen der man dynamische Codes nimmt. Der Scan-Zähler wird beiläufig erhöht; scheitert er, geht die Weiterleitung trotzdem durch.


Landingpages

Fünf Typen führen auf eine Seite unter {BASE_URL}/s/{slug}, die die Anwendung selbst ausliefert.

Diese Seiten sind öffentlich und zeigen bewusst nichts von der Anwendung: keine Navigation, kein „Verwaltung“, kein Hinweis auf den Generator, und im Seitentitel steht der Inhalt statt des Produktnamens. Wer einen Aushang scannt, hat mit dem Generator nichts zu tun.

Technisch liegt das App-Chrome in einer Routengruppe, die Landingpages außerhalb — sie können es also gar nicht versehentlich erben.

Weitere Eigenschaften:

  • Alle Texte kommen aus den Formulardaten und werden als Text ausgegeben, nie als HTML. Eingegebene Zeichen wie <b> erscheinen als solche.
  • Verlinkte Ziele bekommen rel="noopener noreferrer" — kein Zugriff auf das Fenster, aus dem sie geöffnet wurden.
  • Adressen müssen mit http:// oder https:// beginnen; javascript: und Ähnliches wird abgewiesen.
  • Passen die gespeicherten Daten nicht mehr zum Schema, erscheint eine freundliche Erklärung statt eines Fehlers.
  • Der Scan-Zähler zählt den Aufruf des Codes, nicht das Neuladen der Seite.

Video-Landingpage. Der Rahmen zu YouTube oder Vimeo lädt erst nach einem Klick, und davor steht, dass dabei Daten an den Anbieter übertragen werden. Eingebettet wird die Variante ohne Cookies (youtube-nocookie.com). Direkte Videodateien laden mit preload="none" — wer unterwegs scannt, zahlt das Volumen.


Dateien

PDFs und Bilder liegen in der Datenbank, nicht auf der Platte. Damit gibt es weiterhin genau ein Volumen zu sichern: Wer die Datenbank sichert, hat alles. Ein zweites Volumen wird beim Sichern zu leicht vergessen.

Erlaubt PDF; PNG, JPEG, WebP, GIF, AVIF
Nicht erlaubt SVG und alles Übrige
Größe MAX_UPLOAD_MB, Voreinstellung 10 MB
Geprüft wird der Dateiinhalt, nicht die Endung

Warum kein SVG: Eine SVG-Datei darf Skripte enthalten. Ausgeliefert von derselben Adresse wie die Anwendung könnte sie damit auf angemeldete Sitzungen zugreifen.

Wie geprüft wird: Der vom Browser gemeldete Typ kommt aus der Endung und lässt sich beliebig setzen. Entschieden wird deshalb anhand der ersten Bytes der Datei. Eine Textdatei namens harmlos.pdf wird abgewiesen.

Wie ausgeliefert wird: mit dem erkannten Typ, X-Content-Type-Options: nosniff und einer Content-Security-Policy: sandbox — damit auch ein PDF mit eingebetteten Skripten nichts anrichten kann. Ein Anhängen von ?download erzwingt das Speichern statt der Anzeige.

Wem eine Datei gehört: Sie hängt am Code. Wird er gelöscht, verschwindet sie mit ihm. Eine ausgetauschte Datei verliert ihren Bezug und wird nach 24 Stunden weggeräumt — ebenso eine hochgeladene, deren Code nie gespeichert wurde. Eine fremde Datei lässt sich nicht an den eigenen Code hängen; sonst könnte man sie einem anderen Code entziehen.

Die Adresse einer Datei ist nicht erratbar, aber öffentlich abrufbar — dieselbe Überlegung wie beim Slug einer Kurz-URL. Vertrauliche Unterlagen gehören nicht hinter einen QR-Code, den man abfotografieren kann.


Feedback

Ein Feedback-Code erzeugt ein Formular mit fünf Sternen oder fünf Gesichtern und wahlweise einem Textfeld. Überschrift, Frage, Beschriftung des Textfelds und der Text nach dem Absenden sind frei formulierbar.

Gespeichert werden nur die Note von 1 bis 5 und der Text. Kein Name, keine IP-Adresse, keine Gerätekennung — das steht auch für die Antwortenden sichtbar unter dem Formular.

Gegen Fluten gibt es eine Bremse von 10 Rückmeldungen pro Minute und Absender. Die Adresse dient dabei nur als Schlüssel im Arbeitsspeicher und wird nirgends gespeichert. Rückmeldungen zu Codes, die gar kein Feedback einsammeln, werden abgewiesen.

Die Auswertung ist aus der Codeliste erreichbar und zeigt Durchschnitt, Verteilung der Noten und alle Kommentare mit Datum (die 200 jüngsten). Sie sieht nur, wer den Code auch sehen darf.


Gestaltung und Ausgabe

Einstellung Bereich Voreinstellung
Vordergrundfarbe frei (Hex oder Farbwähler) #0F172A
Hintergrundfarbe frei #FFFFFF
Größe 256–2048 px 512 px
Fehlerkorrektur L, M, Q, H M
Rand 0–16 Module 4

Fünf Farbvorschläge (Klassisch, Indigo, Waldgrün, Bordeaux, Marine) und ein Knopf zum Tauschen von Vorder- und Hintergrund. Reicht der Kontrast nicht, warnt die Anwendung — ein hübscher, aber unlesbarer Code nützt niemandem.

Die Vorschau wird serverseitig gerendert und aktualisiert sich bei jeder Änderung. Herunterladen geht als PNG (in der eingestellten Größe) und als SVG (frei skalierbar). Die Größe betrifft nur das PNG.


Meine Codes

Zeigt die dynamischen Codes und die statischen, die abgelegt wurden. Ein abgelegter statischer Code trägt statt einer Kurz-URL das Abzeichen „statisch“ und zählt keine Scans.

  • Zwei Ansichten: Liste (Voreinstellung, eine Zeile je Code) und Karten mit großer Vorschau. Die Wahl merkt sich der Browser.
  • Suche über Name, Ziel, Kurz-URL und Typ.
  • Sortierung nach neueste, älteste, Name oder Scan-Anzahl.
  • Gruppenfilter, sobald Codes in mehr als einer Gruppe liegen; dann gibt es auch eine Gruppenspalte.
  • Filter nach Art (Alle Arten, Nur dynamische, Nur statische), sobald beide Arten vorkommen. Bei „Nur statische“ verschwindet die Scan-Spalte und mit ihr die Sortierung danach: Statische zählen keine Scans, die Spalte wäre eine Reihe aus Strichen.
  • Kurz-URL kopieren mit einem Klick.
  • Ziel ändern — die Kurz-URL und damit der gedruckte Code bleiben gültig.
  • Löschen mit Rückfrage, die den betroffenen Code nennt.
  • PNG und SVG erneut herunterladen.
  • Bei Feedback-Codes ein Weg zur Auswertung.

Auf schmalen Bildschirmen weichen die hinteren Spalten; die Kurz-URL rückt unter den Namen, Bearbeiten und Löschen bleiben erreichbar.


Anmeldung, Gruppen und Rollen

Die Anmeldung ist abschaltbar (AUTH_ENABLED). Bewusst über die Konfiguration und nicht über die Oberfläche: So kann man sich nicht aussperren.

Ohne Anmeldung darf jeder, der die Anwendung erreicht, alles. Für den Einzelplatz- oder LAN-Betrieb ist das gewollt; ins offene Internet gehört die Anwendung dann nicht.

Mit Anmeldung gibt es vier Ebenen:

  1. Benutzer — meldet sich mit E-Mail und Passwort an.
  2. Gruppe — bündelt Codes und die Menschen, die damit arbeiten. Jeder Code gehört genau einer Gruppe.
  3. Rolle je Gruppe:
    Rolle Darf
    Betrachter sehen und herunterladen
    Bearbeiter zusätzlich anlegen, ändern, löschen
    Administrator zusätzlich Mitglieder und Rollen der Gruppe verwalten
  4. Administrator der Installation — legt Gruppen, Benutzer und Domains überhaupt erst an.

Zur Umsetzung:

  • Passwörter werden mit scrypt gehasht (Node-Bordmittel, kein natives Modul). Mindestlänge 10 Zeichen.
  • Nach 8 Fehlversuchen ist das Konto 15 Minuten gesperrt.
  • Sitzungen liegen in der Datenbank, nicht in einem signierten Cookie — so lassen sie sich serverseitig beenden, etwa beim Deaktivieren eines Benutzers. Im Cookie steht ein Zufallstoken, in der Datenbank nur dessen SHA-256. Laufzeit 30 Tage.
  • Der Cookie ist httpOnly und SameSite=Lax; Secure wird gesetzt, sobald BASE_URL mit https:// beginnt.
  • Alle Berechtigungsregeln stehen an einer Stelle (src/lib/auth/policy.ts) — nicht verteilt über die Oberfläche.
  • Im Konto sieht man die eigenen Gruppen und Rollen, ändert sein Passwort und meldet sich ab.

Single Sign-on

Die Anmeldung lässt sich an einen OpenID-Connect-Anbieter abgeben — getestet mit Authentik, ebenso nutzbar mit Keycloak, Zitadel oder Entra ID. Alles Nötige kommt aus dem Discovery-Dokument des Anbieters.

  • Anmeldefluss mit PKCE (S256), state und nonce.
  • Das ID-Token wird gegen den JWKS des Anbieters geprüft: Signatur, Aussteller, Empfänger, Ablauf und Nonce.
  • Wiedererkannt wird jemand an der Kennung des Anbieters (sub), nicht an der E-Mail-Adresse — Adressen ändern sich.
  • Ein bestehendes Konto mit derselben Adresse wird verknüpft statt verdoppelt, aber nur bei bestätigter Adresse. Sonst könnte sich beim Anbieter jemand eine fremde Adresse eintragen und damit ein hiesiges Konto übernehmen.
  • Neue Konten bekommen die Rolle aus OIDC_DEFAULT_ROLE in der Standardgruppe.
  • OIDC_ADMIN_GROUP hängt die Instanz-Administration an eine Gruppe beim Anbieter; sie wird dann bei jeder Anmeldung neu entschieden.
  • OIDC_ONLY=true blendet die Passwort-Anmeldung aus.

Bewusst nicht übernommen werden Gruppen und Rollen. Die Gruppen im Verzeichnis bedeuten etwas anderes als die hier: dort geht es um Organisation und Zugang, hier darum, wer welche Codes sehen und ändern darf. Eine automatische Übersetzung wäre eine Quelle stiller Überraschungen.

→ Einrichtung Feld für Feld: authentik.md


Mehrere Domains

Kurz-URLs können unter verschiedenen Adressen laufen — etwa je Kunde eine. Domains werden in der Verwaltung angelegt und Gruppen zugeteilt.

  • Die Weiterleitung erkennt am Host-Header, welche Domain gemeint ist, und prüft ihn gegen die registrierten Domains. Ein unbekannter Hostname endet mit 404 — über einen gefälschten Header lässt sich also keine fremde Domain ansprechen.
  • Der Slug ist je Domain eindeutig, nicht global.
  • Eine Domain kann mehreren Gruppen dienen. Die Codes bleiben trotzdem strikt nach Gruppe getrennt.
  • Je Gruppe lässt sich eine Domain als Voreinstellung festlegen.
  • Eine Domain lässt sich abschalten, ohne sie zu löschen; ihre Codes antworten dann mit einer Erklärung.

Damit das funktioniert, muss der Reverse Proxy jede dieser Domains auf diese Anwendung leiten.

Kurzhilfe zur Einrichtung

Was außerhalb der Anwendung einzurichten ist, steht nicht nur im README: Auf der Domain-Seite öffnet der Knopf Kurzhilfe eine Erklärung in sechs Schritten — DNS-Eintrag, Weiterleitung im Reverse Proxy samt der drei nötigen Kopfzeilen zum Abtippen, Zertifikat, Registrieren, Gruppenzuteilung, Nachprüfen. Der Hostname aus BASE_URL wird als Beispiel eingesetzt.

Bewusst ein Dialog und kein Dauertext: Die Erklärung braucht man einmal beim Einrichten und danach nie wieder.

Prüfen, ob eine Domain hierher führt

Bei jedem Eintrag steht ein Knopf Prüfen. Er geht den Weg der Scanner nach und meldet vier Dinge:

Schritt Was geprüft wird
DNS Löst der Name auf? Auf welche Adressen?
Zertifikat Gültig, von wem ausgestellt, wie lange noch?
Erreichbar Antwortet dort ein Webserver, und wie schnell?
Ziel Steht dahinter diese Installation?

Der letzte Schritt ist der eigentliche Wert. Dass irgendwo etwas antwortet, sagt wenig — es könnte die Parkseite des Registrars sein oder eine andere Installation dieser Anwendung. Deshalb ruft die Prüfung unter der Domain eine eigene Route auf und lässt sich einen HMAC über eine frische Zufallszahl zurückgeben. Das Geheimnis dafür entsteht beim ersten Check, liegt in der Datenbank und verlässt den Server nie.

Die Meldungen sind im Klartext: abgelaufen, selbst ausgestellt, gilt für einen anderen Namen, Zwischenzertifikat fehlt, Port 443 nimmt nichts an, „es antwortet etwas, aber nicht diese Anwendung“. Läuft ein Zertifikat in höchstens 14 Tagen ab, gilt der Schritt als Warnung statt als Erfolg. Scheitert schon DNS, fallen die drei Folgeschritte aus — dreimal dieselbe Meldung würde die eine wichtige verdecken.

Die Grenze der Aussage steht unter jedem Bericht: Der Aufruf startet dort, wo der Container läuft. Bei internem DNS oder Hairpin-NAT kann er scheitern, obwohl von außen alles stimmt; umgekehrt beweist ein Erfolg von innen nicht, dass die Domain von außen erreichbar ist.

Geprüft wird ausschließlich ein Hostname aus der Datenbank, nie einer aus der Anfrage — sonst wäre die Route ein Werkzeug, mit dem sich vom Server aus beliebige Adressen abklopfen ließen. Näheres in sicherheit.md.


Erscheinungsbild

Unter Verwaltung → Erscheinungsbild lassen sich einstellen:

Name Kopfzeile, Titel des Browser-Tabs, Fußzeile
Zusatz in der Fußzeile steht hinter dem Namen
Akzentfarbe neun Vorschläge oder ein eigener Hex-Wert
Logo PNG, JPEG, WebP oder AVIF bis 2 MB; ersetzt Zeichen und Name
Favicon PNG, ICO oder WebP bis 512 KB

Wie die Farbe wirkt. Tailwind übersetzt bg-indigo-600 zu background-color: var(--color-indigo-600). Die Anwendung überschreibt diese Variablen zur Laufzeit — damit folgen alle rund 60 eingefärbten Stellen der gewählten Farbe, ohne dass eine einzige Klasse angefasst werden müsste.

Aus der Eingabe übernommen werden nur Farbton und Sättigung, nicht die Helligkeit. Die Helligkeitsstufen kommen aus der Vorlage. Das ist Absicht: Auf der Stufe 600 steht weiße Schrift. Käme die Helligkeit aus der Eingabe, würde ein helles Gelb die Kopfzeile unlesbar machen — so bleibt jeder Farbton benutzbar.

Hell und dunkel

Die Anwendung folgt von sich aus der Einstellung des Betriebssystems. In der Kopfzeile steht ein Umschalter mit drei Zuständen:

System folgt dem Betriebssystem — abends dunkel, morgens hell
Hell immer hell
Dunkel immer dunkel

Drei statt zwei, weil ein reiner Ein/Aus-Schalter nach dem ersten Klick für immer eine feste Wahl bedeutet. Die Wahl merkt sich der Browser; ein zweiter Tab zieht automatisch nach.

Wie es umgesetzt ist. Nicht über dark:-Klassen — die hätten an rund 440 Stellen ergänzt werden müssen, und jede neue Zeile müsste daran denken. Stattdessen entscheidet light-dark() im CSS, welchen Wert die Farbvariablen tragen; gesteuert wird das über color-scheme. Deshalb genügt für den Umschalter ein Attribut am <html>-Element — und die Systemwahl funktioniert sogar ohne JavaScript.

Die dunklen Werte sind keine Spiegelung der hellen: Reines Schwarz unter weißer Schrift flimmert. Die Flächen liegen bei 20–26 % Helligkeit im selben leicht blauen Ton wie die helle Reihe. Alle Textkontraste liegen über der Anforderung von 4,5:1 — gemessen, nicht geschätzt.

Unverändert bleiben die QR-Codes selbst. Deren Farben legt die Gestaltung fest, und ein Code, der hell gedruckt wird, soll in der Vorschau nicht anders aussehen.

Auf den Landingpages gelten Farbe, Favicon und das Farbthema, aber weder Logo noch Navigation. Wer einen Aushang scannt, soll die Seite des Inhalts sehen, nicht die einer Verwaltungssoftware. Ein Logo je Gruppe auf den Landingpages steht als eigener Punkt auf der Roadmap.

Zu den Dateien. Logo und Favicon liegen wie alle Uploads in der Datenbank und werden über /api/uploads/{id} ausgeliefert — öffentlich, denn sie stehen auf jeder Seite. Beim Ersetzen wird die vorherige Datei gelöscht. SVG bleibt auch hier ausgeschlossen, aus demselben Grund wie bei den übrigen Uploads.


Verwaltung

Bereich Was dort geht
Einstellungen was Bearbeiter dürfen — etwa eigene Kurz-Links
Erscheinungsbild Name, Logo, Favicon, Akzentfarbe
QR-Typen einzelne Typen ausblenden — für alle oder je Gruppe
Benutzer anlegen, deaktivieren, Passwort setzen, Instanz-Admin vergeben
Gruppen anlegen, umbenennen, Mitglieder und deren Rollen pflegen
Domains anlegen, Gruppen zuteilen, abschalten

Zu den QR-Typen: 15 Kacheln sind viel, wenn ihr nur drei davon braucht. Ein abgeschalteter Typ verschwindet aus der Auswahl; bereits erstellte Codes funktionieren unverändert weiter — sie leiten weiter, zählen Scans und lassen sich bearbeiten. Nur neue lassen sich nicht mehr anlegen. Ohne Eintrag in der Datenbank gilt ein Typ als freigegeben, damit eine frische Installation alles zeigt und Typen aus späteren Versionen automatisch erscheinen.

Die Freigabe hat zwei Ebenen. Die Grundeinstellung gilt für die ganze Installation; je Gruppe lässt sich davon abweichen — die Marketingabteilung bekommt Social Media, die Werkstatt nur Website und PDF. Fehlt eine Abweichung, folgt die Gruppe der Grundeinstellung.

Die globale Abschaltung gewinnt immer. Ein Typ, der in der Grundeinstellung aus ist, lässt sich in keiner Gruppe einschalten; der Schalter steht dort sichtbar still und nennt den Grund. Andersherum wäre „hier abgeschaltet“ keine verlässliche Aussage mehr.

Auf der Startseite sieht man die Vereinigung über die eigenen Gruppen. Wer in beiden Gruppen ist, sieht beides; welche Freigabe zählt, entscheidet sich beim Anlegen anhand der gewählten Gruppe.


Betrieb

  • Zwei Container: die Next.js-Anwendung und PostgreSQL. Kein Reverse Proxy nötig — Port 3000 wird direkt veröffentlicht. Ein Proxy davor geht selbstverständlich auch.
  • Migrationen laufen beim Start von selbst. Es gibt keinen Extraschritt nach docker compose up -d --build.
  • Die Datenbank hat einen Healthcheck; die Anwendung startet erst, wenn sie wirklich Anfragen annimmt.
  • Ein Volumen für die Datenbank — dort liegen auch die hochgeladenen Dateien.
  • Die Konfiguration steht vollständig in einer .env; eine .env.example ist mitgeliefert und dokumentiert jeden Wert.
  • Die Anwendung liest die Werte zur Laufzeit, nicht beim Bauen — eine geänderte BASE_URL braucht keinen neuen Build.
  • Die Oberfläche ist durchgängig deutsch, hell, responsiv und ohne externe Schriften oder Skripte.

Version und Änderungen

Die Fußzeile nennt die laufende Version und führt auf eine Seite mit dem vollständigen Änderungsverlauf. Nach einem Update ist das die schnellste Antwort auf die Frage, ob der Container wirklich mit dem neuen Abbild läuft: Steht dort noch die alte Nummer, ist er es nicht.

Der Verlauf wird an einer Stelle gepflegt (src/lib/changelog.ts). Daraus entstehen sowohl die Seite in der Anwendung als auch die CHANGELOG.md im Projekt (npm run changelog); eine Prüfung wacht darüber, dass beides zusammenpasst. Zu jeder Version gibt es einen Git-Tag.


Schnittstelle

Alles, was die Oberfläche tut, geht auch über HTTP:

Route Zweck
POST /api/qr/preview Vorschau erzeugen (statisch oder dynamisch)
POST /api/qr/download PNG oder SVG erzeugen
GET/POST /api/codes dynamische Codes auflisten und anlegen
GET/PATCH/DELETE /api/codes/{id} einzelnen Code lesen, ändern, löschen
GET /api/codes/{id}/qr Bild eines gespeicherten Codes
POST /api/uploads Datei hochladen (GET nennt die Größengrenze)
GET /api/uploads/{id} Datei ausliefern (?download erzwingt Speichern)
POST /api/feedback Rückmeldung abgeben
GET/POST /api/auth Zustand, Anmelden, Abmelden, Passwort ändern
GET /api/auth/oidc/start · /callback Anmeldung über den Anbieter
/api/admin/types · users · groups · domains · branding · settings Verwaltung
GET /r/{slug} Weiterleitung eines dynamischen Codes
GET /s/{slug} öffentliche Landingpage

Fehler kommen als { "error": "…" } mit einer deutschen Meldung, die sich unverändert anzeigen lässt.


Was die Anwendung bewusst nicht tut

  • Kein Logo im QR-Code. Wer es braucht, nimmt das SVG, setzt es im Layoutprogramm ein, wählt Fehlerkorrektur H und testet mit mehreren Geräten.
  • Keine Statistik über die Zeit. Der Zähler weiß, wie oft und wann zuletzt — kein Verlauf, keine Herkunft, keine Geräte. Das wäre eine eigene Tabelle und die Frage, wie lange man so etwas aufbewahrt.
  • Keine Übernahme von Gruppen aus dem Verzeichnis. Siehe oben.
  • Keine Stapelverarbeitung. Codes entstehen einzeln.
  • Keine Mehrsprachigkeit. Die Oberfläche ist deutsch.
  • Kein Papierkorb. Ein gelöschter Code ist weg, samt seiner Dateien und Rückmeldungen.

Was davon als Nächstes sinnvoll wäre, steht in der Roadmap.