feat(tags): QR-Aufkleber, Geraeteseite, Etikettendruck und Scanner
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

Der Einstieg, fuer den die Anwendung gebaut ist: scannen, sehen was zu tun ist,
loslegen. `/t/<tagCode>` kennt drei Faelle — registriertes Geraet, vorgedruckter
Tag, unbekannter Code —, die Geraeteseite zeigt Zuweisungen, Schnellstart und
Historie, und beides ist am Handy bedienbar.

Im QR steht ausschliesslich `{APP_URL}/t/{tagCode}`: kein Name, keine
Seriennummer, kein Besitzer. Ein Aufkleber haengt jahrelang sichtbar am Geraet
und wird von jedem Telefon gelesen; was darauf steht, ist veroeffentlicht. Die
Masse sind gemessen, nicht behauptet: ein echtes PDF liefert 62 x 29 mm und A4,
und ein bei 300 dpi gerastertes Etikett wird mit demselben Decoder
zurueckgelesen, den auch der Scanner benutzt.

Drei Defekte zeigte erst der gebaute Stand, alle an den Naehten zwischen den
parallel gebauten Teilen: „Starten“ setzte IN_ARBEIT und sprang nicht, „Vorlage
zuweisen“ bot Vorlagen an, die der Dienst danach ablehnte, und die Geraeteliste
verlinkte nichts. Der unabhaengige Review fand drei Blocker — darunter, dass der
Scanner nie die native BarcodeDetector-API benutzte und dass ein Unterpfad in
APP_URL aus dem gedruckten QR herausfiel.

Closes #14

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-25 21:30:12 +02:00
co-authored by Claude Opus 5
parent c78a6ac1af
commit 5f1b0d1435
58 changed files with 4828 additions and 54 deletions
+67 -24
View File
@@ -1,9 +1,9 @@
# #014 M9 – QR-Tags, Geräteseite, Etiketten, Scanner
**Status:** offen
**Status:** erledigt
**Meilenstein:** v0.1.0
**Labels:** phase-1, backend, frontend
**Geschätzt:** 12 h **Tatsächlich:** –
**Geschätzt:** 12 h **Tatsächlich:** 3 h 35 min
## Ziel
@@ -11,28 +11,71 @@ Der Einstieg für den Techniker, der am Gerät steht: scannen, sehen was zu tun
## Aufgaben
- [ ] Stapel-Erzeugung von N unregistrierten Tags (Standard 50) für vorgedruckte Aufkleber
- [ ] Route `/t/[tagCode]` mit Anmeldepflicht und Rücksprung; drei Fälle: registriert → Geräteseite; existiert unregistriert → Registrierformular; unbekannt → Hinweis „Kein Laufzettel-Tag“ mit Link zur Suche
- [ ] Registrierformular: Typ, Hostname oder Bezeichnung, Seriennummer, Inventarnummer, optional Besitzer per Mitarbeitersuche; danach Geräteseite
- [ ] Geräteseite `/geraete/[id]`, handyoptimiert: Kopf mit Hostname bzw. Bezeichnung, Typ, Besitzer mit Abteilung, Seriennummer, `tagCode` in JetBrains Mono
- [ ] Abschnitt „Zugewiesen“: alle Läufe zu diesem Gerät in `OFFEN` und `IN_ARBEIT` mit Fortschritt, Zuweisung und Fälligkeit, Knopf „Starten“ bzw. „Weiter“
- [ ] Abschnitt „Schnellstart“: Vorlagen aus `GeraetetypVorlage` für diesen Gerätetyp, ein Tipp startet den Lauf mit vorbelegtem Gerät und Besitzer
- [ ] Abschnitt „Vorlage zuweisen“: Vorlage wählen, optional Techniker und Fälligkeit, erzeugt einen Lauf in `OFFEN`
- [ ] Abschnitt „Historie“: abgeschlossene und archivierte Läufe chronologisch
- [ ] QR-Erzeugung serverseitig als SVG mit `qrcode`, Fehlerkorrektur M, Inhalt ausschließlich `{APP_URL}/t/{tagCode}`, keine Personendaten, keine externe API
- [ ] Etikettendruck mit Print-CSS in zwei Layouts: Etikettendrucker 62 × 29 mm (ein Etikett pro Seite) und A4-Bogen 3 × 8 à 70 × 37 mm; je Etikett QR, `tagCode` lesbar darunter, optional Hostname und Besitzer, kleines Laufzettel-Logo; immer hell
- [ ] Im Modus `DIENSTLEISTUNG` zeigt das Etikett zusätzlich das Kundenkürzel, im Modus `INTERN` nie
- [ ] Knopf „Etikett drucken“ auf der Geräteseite, Stapeldruck für unregistrierte Tags
- [ ] „Scannen“ in der App: `barcode-detector` (native BarcodeDetector-API mit Polyfill-Fallback), Kamera im Browser, direkter Sprung zur Geräteseite
- [ ] Screenshots Geräteseite Handy, Registrierformular, Etikett 62 × 29, A4-Bogen, Scanner in `docs/screenshots/m9/`
- [x] Stapel-Erzeugung von N unregistrierten Tags (Standard 50) für vorgedruckte Aufkleber
- [x] Route `/t/[tagCode]` mit Anmeldepflicht und Rücksprung; drei Fälle: registriert → Geräteseite; existiert unregistriert → Registrierformular; unbekannt → Hinweis „Kein Laufzettel-Tag“ mit Link zur Suche
- [x] Registrierformular: Typ, Hostname oder Bezeichnung, Seriennummer, Inventarnummer, optional Besitzer per Mitarbeitersuche; danach Geräteseite
- [x] Geräteseite `/geraete/[id]`, handyoptimiert: Kopf mit Hostname bzw. Bezeichnung, Typ, Besitzer mit Abteilung, Seriennummer, `tagCode` in JetBrains Mono
- [x] Abschnitt „Zugewiesen“: alle Läufe zu diesem Gerät in `OFFEN` und `IN_ARBEIT` mit Fortschritt, Zuweisung und Fälligkeit, Knopf „Starten“ bzw. „Weiter“
- [x] Abschnitt „Schnellstart“: Vorlagen aus `GeraetetypVorlage` für diesen Gerätetyp, ein Tipp startet den Lauf mit vorbelegtem Gerät und Besitzer
- [x] Abschnitt „Vorlage zuweisen“: Vorlage wählen, optional Techniker und Fälligkeit, erzeugt einen Lauf in `OFFEN`
- [x] Abschnitt „Historie“: abgeschlossene und archivierte Läufe chronologisch
- [x] QR-Erzeugung serverseitig als SVG mit `qrcode`, Fehlerkorrektur M, Inhalt ausschließlich `{APP_URL}/t/{tagCode}`, keine Personendaten, keine externe API
- [x] Etikettendruck mit Print-CSS in zwei Layouts: Etikettendrucker 62 × 29 mm (ein Etikett pro Seite) und A4-Bogen 3 × 8 à 70 × 37 mm; je Etikett QR, `tagCode` lesbar darunter, optional Hostname und Besitzer, kleines Laufzettel-Logo; immer hell
- [x] Im Modus `DIENSTLEISTUNG` zeigt das Etikett zusätzlich das Kundenkürzel, im Modus `INTERN` nie
- [x] Knopf „Etikett drucken“ auf der Geräteseite, Stapeldruck für unregistrierte Tags
- [x] „Scannen“ in der App: `barcode-detector` (native BarcodeDetector-API mit Polyfill-Fallback), Kamera im Browser, direkter Sprung zur Geräteseite
- [x] Screenshots Geräteseite Handy, Registrierformular, Etikett 62 × 29, A4-Bogen, Scanner in `docs/screenshots/m9/`
## Akzeptanzkriterien
- [ ] Test: `/t/LZ-7K3F9Q` ohne Anmeldung leitet zum Login und danach auf die Geräteseite von `PC-001`
- [ ] Test: „Vorlage zuweisen“ erzeugt einen Lauf in `OFFEN`, der unter „Zugewiesen“ erscheint; „Starten“ öffnet ihn und setzt `IN_ARBEIT`
- [ ] Test: unregistrierter Tag führt zum Registrierformular, danach zur Geräteseite; unbekannter Tag zeigt den Hinweis
- [ ] Test: mehrere Zuweisungen je Gerät sind gleichzeitig möglich
- [ ] Test: der QR-Inhalt ist genau `{APP_URL}/t/{tagCode}` und enthält keine Personendaten
- [ ] Test: ein bei 300 dpi gerastertes 62 × 29-mm-Etikett wird von einem QR-Decoder korrekt zurückgelesen
- [ ] Beide Druckbilder sind sauber, ohne Sidebar, ohne dunkle Flächen, ohne abgeschnittene Etiketten
- [ ] Die Geräteseite ist bei 390 px ohne horizontales Scrollen bedienbar
- [x] Test: `/t/LZ-7K3F9Q` ohne Anmeldung leitet zum Login und danach auf die Geräteseite von `PC-001`
- [x] Test: „Vorlage zuweisen“ erzeugt einen Lauf in `OFFEN`, der unter „Zugewiesen“ erscheint; „Starten“ öffnet ihn und setzt `IN_ARBEIT`
- [x] Test: unregistrierter Tag führt zum Registrierformular, danach zur Geräteseite; unbekannter Tag zeigt den Hinweis
- [x] Test: mehrere Zuweisungen je Gerät sind gleichzeitig möglich
- [x] Test: der QR-Inhalt ist genau `{APP_URL}/t/{tagCode}` und enthält keine Personendaten
- [x] Test: ein bei 300 dpi gerastertes 62 × 29-mm-Etikett wird von einem QR-Decoder korrekt zurückgelesen
- [x] Beide Druckbilder sind sauber, ohne Sidebar, ohne dunkle Flächen, ohne abgeschnittene Etiketten
- [x] Die Geräteseite ist bei 390 px ohne horizontales Scrollen bedienbar
## Ergebnis
Erledigt am 2026-09-25. **930 Tests** (vorher 879) und **54 Ende-zu-Ende-Tests** grün, Screenshots in
`docs/screenshots/m9/`.
Zwei Teile entstanden parallel (QR/Etiketten und Geräteseite/Tag-Einstieg), der Scanner kam von
Hand dazu. Die Fehler saßen erwartungsgemäß an den Nähten — und fielen erst im **gebauten** Stand
auf:
- **„Starten“ startete, sprang aber nicht.** `revalidatePath` baut die Geräteseite neu, dabei
verschwindet der Knopf aus dem Baum — und der Effekt, der in den Lauf springen sollte, kam nie
dazu. Der Sprung passiert jetzt im Dienst (`laufStartenAktion`).
- **„Vorlage zuweisen“ bot Vorlagen an, die der Dienst danach ablehnte.** Vorlagen mit
Pflichtangaben führen jetzt in den Start-Dialog — dieselbe Entscheidung, die der Schnellstart
schon traf.
- **Die Geräteliste verlinkte nichts**, weil es die Detailseite bis dahin nicht gab.
### Der unabhängige Review
Drei Blocker, elf Befunde, mehrere Anmerkungen — alle abgearbeitet. Die wichtigsten:
- **Der Scanner benutzte nie die native `BarcodeDetector`-API.** Geladen war die Ponyfill-Fassung, die
**immer** ihre eigene Klasse zurückgibt; jedes Gerät holte 1,1 MB WebAssembly, auch wo der
Browser einen Decoder mitbringt. Jetzt der Polyfill, der nur einspringt, wenn es keinen gibt.
- **Ein ausgefallener Decoder blieb unsichtbar:** dreimal je Sekunde ein Fehler, und auf dem
Bildschirm stand unverändert „Halte den Aufkleber ins Bild“. Nach drei Fehlschlägen hört die
Abfrage auf und sagt es.
- **Ein Unterpfad in `APP_URL` fiel aus dem QR heraus** — `https://firma.de/laufzettel` wurde zu
`https://firma.de/t/…`. Auf Papier ist das nicht mehr zu berichtigen; `basisAdresse()` hält den
Pfad jetzt fest.
- **Ohne `APP_URL` druckte jedes Etikett stillschweigend `localhost`.** Die Druckseite warnt.
- **Der Stapeldruck war die offene Hintertür zu einer Seite mit Schloss:** `/tags` verlangt
`verwaltung`, der Bogen ohne Codes zeigte dieselbe Liste jedem Leser. Jetzt gilt dort dasselbe
Recht.
- **`/api/qr/[tagCode]` hatte keinen Aufrufer und keinen Test** und faltete Groß/Klein anders als
`/t/<code>`. Entfernt; die Geräteseite setzt das SVG direkt ein — dieselbe Quelle wie das
Etikett, eine Anfrage weniger.
- **Die Besitzerliste beim Registrieren filterte erst nach der Abfrage** auf den Kunden: bei mehr
als 500 Personen wäre keine passende übrig geblieben.
- Dazu vier Texte und Kommentare, die mehr versprachen als der Code hielt (archivierte Geräte im
Druck, „1 von 2 fehlt“ bei doppelten Codes, eine Vorbelegung, die nicht kam, ein verrutschter
Kommentarblock).
+2 -2
View File
@@ -18,10 +18,10 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
| #011 | [M6 – Mitarbeiter, Abteilungen, Geräte](011-mitarbeiter-abteilungen-geraete.md) | erledigt | v0.1.0 | 9 h | 2 h 15 min |
| #012 | [M7 – Platzhalter, Namensregeln, Escaping](012-platzhalter-namensregeln.md) | erledigt | v0.1.0 | 8 h | 2 h 30 min |
| #013 | [M8 – Läufe: Service und Abarbeitung](013-laeufe-abarbeitung.md) | erledigt | v0.1.0 | 14 h | 3 h 05 min |
| #014 | [M9 – QR-Tags, Geräteseite, Etiketten, Scanner](014-qr-tags-geraeteseite.md) | offen | v0.1.0 | 12 h | – |
| #014 | [M9 – QR-Tags, Geräteseite, Etiketten, Scanner](014-qr-tags-geraeteseite.md) | erledigt | v0.1.0 | 12 h | 3 h 35 min |
| #015 | [M10 – Dashboard und Archiv](015-dashboard-archiv.md) | offen | v0.1.0 | 8 h | – |
| #016 | [M11 – Verzeichnis und LDAP-Abgleich](016-verzeichnis-ldap.md) | offen | v0.1.0 | 12 h | – |
| #017 | [M12 – Seed der sieben Vorlagen](017-seed-sieben-vorlagen.md) | offen | v0.1.0 | 8 h | – |
| #018 | [M13 – E2E-Tests, Anleitung, PDF](018-e2e-anleitung-pdf.md) | offen | v0.1.0 | 14 h | – |
| #019 | [M14 – Betriebswege und Release v0.1.0](019-betrieb-release.md) | offen | v0.1.0 | 10 h | – |
| | **Summe** | | | **136 h** | **16 h 50 min** |
| | **Summe** | | | **136 h** | **20 h 25 min** |