Files
qrcode-generator/README.md
T
Florian Wilde 94a373ff48 README: Schritt-fuer-Schritt-Anleitung zum Start
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.
2026-08-14 11:47:44 +02:00

641 lines
24 KiB
Markdown

# QR-Code-Generator
Eine Webanwendung zum Erzeugen von QR-Codes — als Docker-Compose-Stack aus
zwei Containern: der Next.js-Anwendung und einer PostgreSQL-Datenbank.
Umgesetzt sind **9 der 15 QR-Typen**. Die übrigen sechs sind in der Oberfläche
sichtbar, als Platzhalter markiert und lassen sich über die Verwaltung
ausblenden.
---
## Inhalt
- [Schritt für Schritt zum laufenden Stack](#schritt-für-schritt-zum-laufenden-stack)
- [Hinter einem Reverse Proxy (Nginx Proxy Manager)](#hinter-einem-reverse-proxy-nginx-proxy-manager)
- [Wenn etwas nicht läuft](#wenn-etwas-nicht-läuft)
- [Aktualisieren](#aktualisieren)
- [Konfiguration](#konfiguration)
- [Bedienung](#bedienung)
- [Technischer Aufbau](#technischer-aufbau)
- [Entwicklung ohne Docker](#entwicklung-ohne-docker)
- [API](#api)
- [Ausblick Version 2](#ausblick-version-2)
---
## Schritt für Schritt zum laufenden Stack
Vom leeren Server bis zum ersten QR-Code. Alle Befehle laufen auf dem
Rechner, auf dem die Anwendung später läuft.
### Schritt 1 — Voraussetzungen prüfen
Gebraucht werden Docker mit Compose v2 und Git:
```bash
docker compose version # erwartet: Docker Compose version v2.x oder höher
git --version
```
Kommt `docker: command not found`, fehlt Docker — die
[offizielle Anleitung](https://docs.docker.com/engine/install/) installiert
Engine und Compose in einem Rutsch.
### Schritt 2 — Projekt holen
```bash
git clone https://git.wilde-server.de/florian.wilde/qrcode-generator.git
cd qrcode-generator
```
### Schritt 3 — Konfiguration anlegen
```bash
cp .env.example .env
nano .env
```
Zwei Werte **musst** du anfassen:
| Wert | Was eintragen |
| --- | --- |
| `POSTGRES_PASSWORD` | Ein eigenes Passwort. Es steht nur in dieser Datei und wird nirgends sonst gebraucht. |
| `DATABASE_URL` | Dasselbe Passwort noch einmal — es steckt mitten im Verbindungsstring. |
Und einen solltest du bewusst setzen:
| Wert | Was eintragen |
| --- | --- |
| `BASE_URL` | Die Adresse, unter der die Anwendung **von außen** erreichbar ist. |
Beispiel für ein eigenes Passwort `MeinGeheimes123`:
```bash
POSTGRES_PASSWORD=MeinGeheimes123
DATABASE_URL=postgresql://qrcode:MeinGeheimes123@db:5432/qrcode?schema=public
BASE_URL=http://192.168.1.50:3000
```
> **Warum `BASE_URL` so wichtig ist:** Ein dynamischer QR-Code enthält nicht
> das eigentliche Ziel, sondern die Kurz-URL `{BASE_URL}/r/{slug}`. Steht dort
> `localhost`, funktioniert der Code **nur auf dem Server selbst** — ein Handy
> läuft ins Leere. Trage deshalb die LAN-Adresse des Servers oder die
> öffentliche Domain ein. Bereits gedruckte Codes lassen sich später nicht
> mehr umbiegen, also am besten vor dem ersten produktiven Code festlegen.
Der Hostname `db` im `DATABASE_URL` bleibt unverändert — das ist der
Servicename aus der `docker-compose.yml`, nicht dein Server.
### Schritt 4 — Starten
```bash
docker compose up -d --build
```
Der erste Durchlauf baut das Image und dauert je nach Maschine **5 bis 15
Minuten**. Spätere Starts brauchen nur wenige Sekunden.
### Schritt 5 — Prüfen, ob alles läuft
```bash
docker compose ps
```
Erwartet werden zwei Container, `db` mit dem Zusatz `(healthy)`:
```
NAME SERVICE STATUS
qrcode-generator-app-1 app Up 30 seconds
qrcode-generator-db-1 db Up 45 seconds (healthy)
```
Und im Protokoll:
```bash
docker compose logs app
```
```
→ Datenbankmigrationen werden angewendet …
All migrations have been successfully applied.
→ Anwendung wird gestartet …
▲ Next.js 16.3.0
✓ Ready
```
Die Datenbank richtet sich beim ersten Start selbst ein — es gibt keinen
Extraschritt dafür.
### Schritt 6 — Aufrufen
Im Browser die Adresse öffnen, die du als `BASE_URL` eingetragen hast, zum
Beispiel <http://192.168.1.50:3000>. Es erscheint „Was möchtest du
erstellen?“ mit den Typ-Kacheln.
### Schritt 7 — Ersten Code erstellen
1. Kachel **Website** anklicken.
2. Adresse eingeben, etwa `https://example.com` — die Vorschau erscheint
sofort.
3. **PNG** oder **SVG** herunterladen. Fertig.
Für einen Code, dessen Ziel du später ändern kannst:
1. Unter „Art des Codes“ auf **Dynamisch** wechseln.
2. Optional einen Namen vergeben, damit du ihn wiederfindest.
3. **Dynamischen Code speichern** — erst dabei entsteht die Kurz-URL, deshalb
ist der Download bis dahin gesperrt.
4. Danach herunterladen. Unter **Meine Codes** lässt sich das Ziel jederzeit
ändern, ohne den gedruckten Code anzufassen.
### Schritt 8 — Aufräumen, was du nicht brauchst
Unter **Verwaltung → QR-Typen** kannst du einzelne Typen ausblenden — etwa
die sechs, die noch nicht umgesetzt sind, damit sie gar nicht erst
auftauchen.
### Die wichtigsten Befehle im Betrieb
```bash
docker compose logs -f app # Protokoll mitlesen (Abbruch mit Strg+C)
docker compose ps # Status beider Container
docker compose restart app # Anwendung neu starten
docker compose down # Stack stoppen — die Daten bleiben erhalten
docker compose up -d # wieder starten
docker compose down -v # Stack stoppen UND die Datenbank löschen
```
---
## Hinter einem Reverse Proxy (Nginx Proxy Manager)
Soll die Anwendung unter einer eigenen Domain mit HTTPS laufen, übernimmt der
Proxy die Zertifikate; die Anwendung selbst bleibt unverändert.
**1. Port nur lokal veröffentlichen.** Wenn Proxy und Anwendung auf demselben
Rechner laufen, muss Port 3000 nicht nach außen offen sein. In der `.env`:
```bash
APP_PORT=3000
BASE_URL=https://qr.deine-domain.de
```
**2. Im Nginx Proxy Manager** einen *Proxy Host* anlegen:
| Feld | Wert |
| --- | --- |
| Domain Names | `qr.deine-domain.de` |
| Scheme | `http` |
| Forward Hostname / IP | IP des Docker-Hosts, z. B. `192.168.1.50` |
| Forward Port | `3000` |
| Websockets Support | an |
| Block Common Exploits | an |
| SSL | Zertifikat anfordern, *Force SSL* aktivieren |
Läuft der Proxy als Container im selben Docker-Netz wie dieser Stack, kann
statt der IP auch der Servicename `app` mit Port `3000` stehen — dann muss
das Port-Mapping gar nicht nach außen offen sein.
**3. `BASE_URL` auf die öffentliche Adresse setzen** — mit `https://` und ohne
Port. Danach:
```bash
docker compose up -d
```
> `BASE_URL` muss zur Adresse passen, unter der Besucher die Anwendung
> aufrufen. Steht dort weiterhin `http://192.168.1.50:3000`, zeigen neue
> dynamische Codes ins interne Netz statt auf die Domain.
Mehrere Domains gleichzeitig unterstützt diese Version noch nicht — das kommt
in Phase B (siehe [Ausblick](#ausblick-version-2)). Zurzeit gilt genau eine
`BASE_URL`.
---
## Wenn etwas nicht läuft
**`POSTGRES_PASSWORD fehlt — bitte .env aus .env.example anlegen`**
Die `.env` fehlt oder ist leer. Schritt 3 nachholen.
**`ports are not available: ... bind: address already in use`**
Port 3000 ist belegt. Entweder den anderen Dienst beenden oder in der `.env`
einen anderen Port setzen, z. B. `APP_PORT=3100`, dann `docker compose up -d`.
**Der Container `db` bleibt `unhealthy` oder startet immer neu**
Meist ein Datenverzeichnis aus einer älteren PostgreSQL-Version. Protokoll
ansehen mit `docker compose logs db`. Wenn dort von einem inkompatiblen
Datenlayout die Rede ist und du die Daten nicht brauchst:
`docker compose down -v` — Achtung, das löscht die Datenbank.
**Der QR-Code führt auf dem Handy ins Leere**
`BASE_URL` zeigt auf `localhost`. Auf die LAN-Adresse oder Domain ändern,
`docker compose up -d`, und die betroffenen dynamischen Codes neu
herunterladen. Statische Codes sind davon nicht betroffen.
**Die Anwendung antwortet nicht, `docker compose ps` zeigt sie aber als `Up`**
`docker compose logs app` ansehen. Kommt dort ein Datenbankfehler, stimmt
meist das Passwort in `DATABASE_URL` nicht mit `POSTGRES_PASSWORD` überein —
beide Werte müssen identisch sein.
**Nach einem Update fehlt etwas oder die Seite ist leer**
`docker compose up -d --build` erzwingt einen Neubau. Hilft das nicht:
`docker compose down && docker compose up -d --build`.
---
## Aktualisieren
Für neue Versionen liegt ein Update-Skript bei — einmal für Linux/macOS und
einmal für Windows. Es holt den aktuellen Stand aus Git, baut den Stack neu
und wartet, bis die Anwendung wieder antwortet.
```bash
./update.sh # Linux / macOS
```
```powershell
.\update.ps1 # Windows
```
Beim ersten Aufruf unter Linux muss das Skript einmal ausführbar gemacht
werden: `chmod +x update.sh`.
Was das Skript tut:
1. prüft, ob `git`, `docker compose` und die `.env` vorhanden sind,
2. bricht ab, wenn es **lokale Änderungen** gibt — sonst würde `git pull` im
Konflikt enden,
3. holt den neuen Stand mit `git merge --ff-only` und zeigt die neuen Commits,
4. baut den Stack nur neu, wenn es tatsächlich Änderungen gab,
5. wartet, bis die Anwendung auf `APP_PORT` antwortet, und zeigt sonst das
Protokoll,
6. gibt die Migrationszeilen aus dem Start-Protokoll und den Containerstatus
aus.
Optionen:
| Option | Wirkung |
| ------------------- | -------------------------------------------------------------- |
| `--force` / `-Force` | Auch ohne neue Commits neu bauen |
| `--prune` / `-Prune` | Danach verwaiste Docker-Images entfernen (`docker image prune`) |
| `--help` | Hilfe anzeigen |
Die Datenbank bleibt beim Update erhalten — neue Migrationen wendet der
App-Container beim Start automatisch an. Wer lieber von Hand aktualisiert:
```bash
git pull
docker compose up -d --build
```
---
## Konfiguration
Alle Einstellungen stehen in der `.env` im Projektverzeichnis (Vorlage:
`.env.example`).
| Variable | Bedeutung | Standard |
| ------------------- | ------------------------------------------------------------- | ------------------------ |
| `BASE_URL` | Öffentliche Adresse der App — landet in dynamischen QR-Codes | `http://localhost:3000` |
| `APP_PORT` | Host-Port, auf dem die App veröffentlicht wird | `3000` |
| `DATABASE_URL` | Verbindungsstring der App zur Datenbank | — |
| `POSTGRES_USER` | Datenbankbenutzer | `qrcode` |
| `POSTGRES_PASSWORD` | Datenbankpasswort (**muss gesetzt sein**) | — |
| `POSTGRES_DB` | Name der Datenbank | `qrcode` |
### Wichtig: `BASE_URL` richtig setzen
Ein dynamischer QR-Code enthält nicht das eigentliche Ziel, sondern die
Kurz-URL `{BASE_URL}/r/{slug}`. Diese Adresse muss von den Geräten erreichbar
sein, die den Code später scannen.
- Nur am selben Rechner testen → `http://localhost:3000` genügt.
- Im LAN mit dem Handy scannen → die LAN-Adresse des Servers eintragen, z. B.
`http://192.168.1.50:3000`.
Wird `BASE_URL` nachträglich geändert, zeigen bereits **gedruckte** dynamische
Codes weiterhin auf die alte Adresse. Am besten also vor dem ersten
produktiven Code festlegen.
Nach einer Änderung an der `.env`:
```bash
docker compose up -d
```
Ändern sich `POSTGRES_*`-Werte, muss `DATABASE_URL` entsprechend mitgezogen
werden — der Hostname darin bleibt `db` (der Servicename aus
`docker-compose.yml`).
---
## Bedienung
### 1. Typ wählen
Die Startseite zeigt unter „Was möchtest du erstellen?“ ein Kachel-Grid der
freigegebenen QR-Typen. Umgesetzte Typen sind anklickbar, die übrigen tragen
das Badge „Bald verfügbar“.
| Typ | Kodiert als | Statisch | Dynamisch |
| --- | --- | :-: | :-: |
| Website | die URL selbst | ✓ | ✓ |
| E-Mail | `mailto:` mit Betreff und Text | ✓ | ✓ |
| SMS | `SMSTO:` | ✓ | ✓ |
| WhatsApp | `https://wa.me/…` | ✓ | ✓ |
| Standort | `geo:` — dynamisch über OpenStreetMap | ✓ | ✓ |
| vCard | vCard 3.0 | ✓ | — |
| WLAN | `WIFI:` | ✓ | — |
| App-Link | Weiterleitung je nach Gerät | — | ✓ |
| Zahlung | GiroCode (EPC069-12) oder Zahlungslink | ✓ | nur Link |
Warum manche Typen nur eine Betriebsart können:
- **vCard und WLAN** werden vom Gerät direkt ausgewertet — die Kamera legt
einen Kontakt an oder richtet das Netz ein. Über eine Kurz-URL würde
stattdessen nur ein Browser aufgehen.
- **App-Link** kann nicht statisch sein: welches Ziel richtig ist, steht erst
beim Scannen fest. Die Weiterleitung wertet den User-Agent aus und schickt
iOS in den App Store, Android zu Google Play und alles Übrige auf die
Ausweichadresse.
- **GiroCode** ist kein Web-Ziel, sondern ein Datensatz für Banking-Apps. Als
dynamischer Code funktioniert daher nur die Variante „Zahlungslink“ — die
Anwendung weist das beim Speichern ausdrücklich ab, statt einen Code
entstehen zu lassen, der beim Scannen ins Leere führt.
> **Zum WLAN-Code:** Das Passwort steht im Klartext im Code — wer ihn scannt
> oder abfotografiert, kennt es. Für Gästenetze ist das gewollt. Besteht ein
> Netzwerkname oder Passwort ausschließlich aus Hex-Ziffern (etwa `abc123`),
> setzt die Anwendung ihn wie in der Spezifikation vorgesehen in
> Anführungszeichen — sonst würden manche Geräte den Wert als Hexzahl deuten.
### 2. Statisch oder dynamisch
| | Statisch | Dynamisch |
| ---------------------- | -------------------------------------------- | ---------------------------------------------------- |
| Im Code kodiert | die Ziel-URL selbst | die Kurz-URL `{BASE_URL}/r/{slug}` |
| Ziel später änderbar | nein | ja, jederzeit |
| Braucht diesen Server | nein | ja — er leitet weiter |
| Scans werden gezählt | nein | ja |
| Wird gespeichert | nein | ja, in PostgreSQL |
Ein dynamischer Code muss einmal **gespeichert** werden, denn erst dabei
entsteht seine Kurz-URL. Bis dahin zeigt die Vorschau einen Platzhalter und
der Download ist gesperrt — so kann kein Code entstehen, der ins Leere führt.
### 3. Gestalten
- **Farben**: Vorder- und Hintergrund per Farbwähler oder als Hex-Wert.
Fünf Voreinstellungen und ein Knopf zum Tauschen der Farben stehen bereit.
- **Kontrastwarnung**: Die Anwendung berechnet das Kontrastverhältnis und
warnt bei schlecht scanbaren Kombinationen — ebenso bei hellem Vordergrund
auf dunklem Grund, den viele Scanner-Apps nicht erkennen.
- **Größe**: 256 bis 2048 px (gilt für das PNG; das SVG skaliert verlustfrei).
- **Fehlerkorrektur**: L, M (Standard), Q oder H. Höhere Level machen den Code
robuster gegen Beschädigung, aber auch dichter.
- **Rand**: Breite der Ruhezone in Modulen. Weniger als 4 kann das Scannen
erschweren.
Die Vorschau wird bei jeder Änderung serverseitig neu erzeugt — sie zeigt also
exakt das, was auch heruntergeladen wird.
### 4. Herunterladen
**PNG** für Druck und Office-Dokumente, **SVG** für verlustfreie Skalierung
(Plakate, Schilder, Druckvorstufe).
### 5. Verwaltung: welche Typen erscheinen
Unter **Verwaltung → QR-Typen** lässt sich jeder der 15 Typen einzeln
ein- und ausblenden. Ausgeblendete Typen verschwinden vollständig von der
Startseite und lassen sich nicht mehr neu anlegen — praktisch, um die noch
nicht umgesetzten Typen gar nicht erst zu zeigen.
Zwei Eigenschaften sind dabei zu unterscheiden:
| | Bedeutung | Woher |
| --- | --- | --- |
| **umgesetzt** | Der Typ ist ausprogrammiert. In Version 1 gilt das nur für „Website“. | Registry im Code |
| **freigegeben** | Die Verwaltung zeigt den Typ an. | Datenbank, jederzeit änderbar |
Nur umgesetzt **und** freigegeben heißt benutzbar. Freigegeben, aber nicht
umgesetzt, erscheint als Kachel „Bald verfügbar“.
> **Bereits gespeicherte Codes bleiben unberührt.** Schaltet man einen Typ ab,
> leiten vorhandene dynamische Codes weiter wie bisher und lassen sich weiter
> bearbeiten. Ein gedruckter Code wird durch das Abschalten seines Typs also
> nicht ungültig — gesperrt ist nur das Anlegen neuer Codes.
Ohne Eintrag in der Datenbank gilt ein Typ als freigegeben. Eine frische
Installation zeigt also alles, und Typen aus späteren Versionen erscheinen
automatisch.
### 6. Meine Codes
Unter „Meine Codes“ stehen alle dynamischen Codes mit Vorschaubild,
Kurz-URL, aktuellem Ziel, Scan-Anzahl, Erstellungsdatum und letztem Scan.
Dort lässt sich
- das **Ziel ändern** — die Kurz-URL und damit der gedruckte Code bleiben
gültig,
- der Code **löschen** (mit Bestätigungsdialog),
- das Bild erneut als PNG oder SVG **herunterladen**.
Statische Codes erscheinen hier nicht, weil sie nicht gespeichert werden.
> Version 1 ist für den Einzelplatz-/LAN-Betrieb gedacht und hat **keine
> Benutzerverwaltung**. Wer die Anwendung erreicht, sieht und ändert alle
> Codes. Die App gehört daher nicht ungeschützt ins offene Internet.
---
## Technischer Aufbau
### Stack
| Baustein | Verwendet |
| ------------ | ------------------------------------------------------- |
| Framework | Next.js 16 (App Router, TypeScript), Frontend + API |
| Oberfläche | React 19, Tailwind CSS 4 |
| Datenbank | PostgreSQL 18 |
| ORM | Prisma 7 mit `@prisma/adapter-pg` |
| QR-Erzeugung | `qrcode`, serverseitig, Ausgabe als PNG und SVG |
| Validierung | Zod 4, im Browser und noch einmal auf dem Server |
Der Stack läuft ohne Reverse Proxy: Der App-Container wird direkt per
Port-Mapping veröffentlicht.
### Verzeichnisse
```
prisma/
schema.prisma Datenmodell (Modell QrCode)
migrations/ Versionierte Migrationen
docker/
entrypoint.sh Migration + Serverstart im Container
update.sh / update.ps1 Update-Skript (git pull + Neubau + Prüfung)
src/
app/
page.tsx Startseite mit dem Kachel-Grid
erstellen/[type]/ Generator je QR-Typ
meine-codes/ Übersicht der dynamischen Codes
r/[slug]/route.ts Weiterleitung dynamischer Codes (302)
api/
qr/preview/ Live-Vorschau (SVG als Data-URL)
qr/download/ Download als PNG oder SVG
codes/ Anlegen und Auflisten dynamischer Codes
codes/[id]/ Ändern und Löschen
codes/[id]/qr/ Bild eines gespeicherten Codes
components/
qr-forms/ Formulare je QR-Typ + deren Registry
qr-generator.tsx Generator-Oberfläche
codes-list.tsx „Meine Codes“
design-panel.tsx Farben, Größe, Fehlerkorrektur, Rand
lib/
qr/
type-module.ts Vertrag eines QR-Typs (defineQrType)
registry.ts Registry aller 15 Typen
types/website.ts Der einzige aktive Typ in V1
design.ts Gestaltungsoptionen + Zod-Schema
contrast.ts Kontrastbewertung
render.ts PNG-/SVG-Erzeugung
request.ts Anfrage-Schema und Auflösung des Code-Inhalts
db.ts Prisma-Client
env.ts Laufzeit-Konfiguration
```
### Datenmodell
Nur dynamische Codes werden gespeichert. Das Modell `QrCode` hält neben Slug,
Zähler und Gestaltung ein generisches `data`-Feld (JSONB) sowie den Typ — so
brauchen die Typen aus Version 2 **keine** Schemaänderung.
### Wie ein QR-Typ aufgebaut ist
Jeder Typ ist ein Modul, das über `defineQrType` erzeugt wird und vier Dinge
beisteuert: Metadaten (Titel, Beschreibung, Icon), ein Zod-Schema, die
Kodierung des Inhalts und eine Kurzfassung für Listen.
```ts
export const websiteType = defineQrType({
id: "website",
title: "Website",
description: "Verlinke eine beliebige Webadresse.",
icon: "globe",
active: true,
supportsDynamic: true,
schema: websiteDataSchema,
defaultData: { url: "" },
encode: (data) => data.url,
summary: (data) => describe(data.url),
});
```
Innerhalb des Moduls arbeitet man mit dem echten, aus dem Zod-Schema
abgeleiteten Datentyp; nach außen zeigt die Registry eine einheitliche
Schnittstelle. Deshalb kennen Vorschau, Download, Speicherung und
Weiterleitung keinen einzigen typspezifischen Sonderfall.
### Warum 302 und kein 301?
Die Weiterleitung unter `/r/{slug}` antwortet mit **302 (temporär)** und
`Cache-Control: no-store`. Ein permanenter Redirect würde in Browsern hängen
bleiben: Zieländerungen kämen nicht an und der Scan-Zähler bliebe stehen.
---
## Entwicklung ohne Docker
Voraussetzung: Node.js 20.9+ und eine erreichbare PostgreSQL-Instanz.
```bash
npm install
# Datenbank z. B. als Einzelcontainer starten
docker run -d --name qr-db -p 5432:5432 \
-e POSTGRES_USER=qrcode -e POSTGRES_PASSWORD=devpass -e POSTGRES_DB=qrcode \
postgres:18-alpine
# .env anlegen und DATABASE_URL auf localhost zeigen lassen:
# DATABASE_URL=postgresql://qrcode:devpass@localhost:5432/qrcode?schema=public
npm run db:migrate # Migrationen anwenden und Prisma Client erzeugen
npm run dev # http://localhost:3000
```
Weitere Skripte:
```bash
npm run build # Produktionsbuild
npm run typecheck # TypeScript prüfen
npm run lint # ESLint
npm run db:studio # Prisma Studio (Datenbank ansehen)
```
> **Hinweis zu npm 11:** Installationsskripte sind standardmäßig blockiert.
> Die Pakete, die welche ausführen dürfen (Prisma lädt darüber seine
> Engine-Binaries), stehen im Feld `allowScripts` der `package.json`.
---
## API
Alle Endpunkte antworten im Fehlerfall mit `{ "error": "…" }` und einer
deutschen Meldung.
| Methode | Pfad | Zweck |
| -------- | ----------------------- | ---------------------------------------------- |
| `POST` | `/api/qr/preview` | Vorschau als SVG-Data-URL |
| `POST` | `/api/qr/download` | PNG- oder SVG-Datei |
| `GET` | `/api/codes` | Alle dynamischen Codes |
| `POST` | `/api/codes` | Dynamischen Code anlegen |
| `GET` | `/api/codes/{id}` | Einzelnen Code lesen |
| `PATCH` | `/api/codes/{id}` | Ziel, Name oder Gestaltung ändern |
| `DELETE` | `/api/codes/{id}` | Code löschen |
| `GET` | `/api/codes/{id}/qr` | Bild des Codes (`?format=png\|svg&download=1`) |
| `GET` | `/r/{slug}` | Weiterleitung, erhöht den Scan-Zähler |
| `GET` | `/api/admin/types` | Zustand aller QR-Typen |
| `PATCH` | `/api/admin/types` | Typ freigeben oder abschalten |
Beispiel:
```bash
curl -X POST http://localhost:3000/api/codes \
-H 'Content-Type: application/json' \
-d '{
"type": "website",
"data": { "url": "https://example.com" },
"design": { "fgColor": "#0F172A", "bgColor": "#FFFFFF",
"size": 512, "ecLevel": "M", "margin": 4 },
"label": "Plakat Eingang"
}'
```
---
## Ausblick Version 2
Geplant sind die übrigen 14 Typen: vCard, App-Link, Mehrere Links, Social
Media, WhatsApp, PDF, Bild, Video, E-Mail, SMS, Standort, Zahlung, WLAN und
Feedback.
Die Architektur ist darauf ausgelegt — ein neuer Typ besteht aus:
1. **einem Modul** in `src/lib/qr/types/` (Zod-Schema, Kodierung, Kurzfassung),
2. **einer Formular-Komponente** in `src/components/qr-forms/`,
3. **je einem Eintrag** in `src/lib/qr/registry.ts` (ersetzt dort den
`defineComingSoonType`-Platzhalter) und in
`src/components/qr-forms/index.ts`.
Am bestehenden Code aus Version 1 ist dafür nichts zu ändern: Vorschau,
Download, Gestaltung, Speicherung dynamischer Codes und die Weiterleitung
arbeiten für alle Typen gleich.
Typen, die statt einer Weiterleitung eine eigene Landingpage brauchen (etwa
„Mehrere Links“ oder „Feedback“), setzen dafür `redirectTarget` auf eine
interne Adresse — auch dafür ist keine Änderung am Datenmodell nötig, weil
`data` als JSONB gespeichert wird.