Bon fuer die Kasse auf einem USB-Bondrucker ausgeben

Der Kunde soll mit einem Zettel zur Kasse gehen koennen, auf dem steht, was
er zahlen muss. Drei Wege fuehren jetzt zu einem Bon: der Knopf "Bon" auf der
Timer-Karte druckt den Zwischenstand und laesst den Timer weiterlaufen,
"Beenden" druckt den Endbeleg, und in der Historie laesst sich jeder Beleg
nachdrucken.

Der Drucker haengt per USB an einem Raspberry Pi und ist damit vom
Timer-Server aus nicht direkt erreichbar. Auf dem Pi laeuft deshalb eine
kleine Bruecke (deploy/bondrucker/, nur Python-Standardbibliothek), die
fertige ESC/POS-Daten per HTTP annimmt. Das Bon-Layout bleibt bewusst hier
im Repo, damit Aenderungen daran nicht auf dem Pi angefasst werden muessen.

Beim Beenden wird erst gespeichert und dann gedruckt. Eine leere Papierrolle
ist damit ein Papierproblem und kein Datenproblem - der Vorgang steht in der
Historie, die Oberflaeche sagt ausdruecklich, dass der Bon nicht gedruckt
wurde, und nennt den Grund.

Ohne PRINTER_HOST in der .env ist die Druckfunktion aus und die App verhaelt
sich unveraendert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kerim (portable/claudecode)
2026-09-04 20:37:12 +02:00
co-authored by Claude Opus 5
parent 5e83999950
commit 70756f3762
13 changed files with 1424 additions and 27 deletions
+62 -6
View File
@@ -12,7 +12,9 @@ Kleine, in sich geschlossene Node-App zur Zeiterfassung mit Kostenberechnung,
gedacht für den Einsatz im Ladengeschäft (Tresen-Tablet, mehrere Plätze parallel).
* **Keine externen Dienste.** Kein Cloud-Backend, keine API-Keys, keine Telemetrie,
keine CDN-Einbindung. Die App macht **keine ausgehenden Netzwerkverbindungen**.
keine CDN-Einbindung. Ausgehend spricht die App nur mit zwei Zielen im
eigenen Netz: Authentik (Anmeldung) und — falls eingerichtet — dem
Bondrucker-Pi (Abschnitt 6). Kein Ziel außerhalb des eigenen Netzes.
* **Keine Datenbank-Installation nötig.** SQLite, eine einzelne Datei.
* **Keine Build-Kette.** Kein Webpack, kein TypeScript, kein npm run build.
Vanilla JS im Frontend, Express im Backend.
@@ -205,6 +207,11 @@ Passwort fehlen — lieber laut scheitern als unsicher laufen.
| `LOGIN_WINDOW_MIN` | `15` | Länge des Zeitfensters in Minuten |
| `DB_PATH` | `../data/timer.db` | Pfad zur SQLite-Datei |
| `RATE_PER_10MIN` | `10` | **Nur beim allerersten Start.** Danach steht der Tarif in der Datenbank und wird über die Oberfläche gepflegt. |
| `PRINTER_HOST` | leer | IP des Bondrucker-Pi. **Leer = Druckfunktion aus**, die App verhält sich wie vorher |
| `PRINTER_PORT` | `9100` | Port der Brücke auf dem Pi |
| `PRINTER_TOKEN` | leer | muss mit `PRINT_TOKEN` in `/etc/bondrucker/env` übereinstimmen |
| `PRINTER_WIDTH` | `42` | Zeichen je Zeile: `42` bei 80-mm-Papier, `32` bei 58 mm |
| `PRINTER_TIMEOUT_MS` | `6000` | Abbruch, wenn der Pi nicht antwortet |
`SESSION_SECRET` muss ein *echter* Zufallswert sein. In der Vorgängerversion
stand dort versehentlich der literale Text `$(openssl rand -base64 32)` — die
@@ -250,6 +257,43 @@ Wichtig in beiden Fällen:
* `/health` ist der Endpunkt für Health-Checks (antwortet **ohne** Anmeldung)
* Websockets werden **nicht** gebraucht
### Bondrucker
Vollständige Einrichtung: **`deploy/bondrucker/README.md`**. Hier nur das
Wesentliche.
Der Drucker hängt per USB an einem Raspberry Pi im Laden. Auf dem Pi läuft ein
kleiner Python-Dienst, der fertige ESC/POS-Daten per HTTP annimmt und in
`/dev/usb/lp0` schreibt. Der Bon selbst entsteht in dieser App
(`backend/receipt.js`) — auf dem Pi liegt bewusst keine Layout-Logik.
```
Timer-App ──HTTP POST /print──▶ Pi (bondrucker.service) ──▶ /dev/usb/lp0
receipt.js baut den Bon schiebt Bytes durch Epson TM-T88IV
```
Drei Wege führen zu einem Bon:
| Auslöser | Beleg | Timer danach |
|---|---|---|
| **🖨 Bon** auf der Timer-Karte | Zwischenstand | läuft unverändert weiter |
| **✓ Beenden** | Endbeleg | ist beendet und in der Historie |
| **🖨** in der Historie | Nachdruck desselben Belegs | — |
**Reihenfolge ist Absicht:** Beim Beenden wird erst gespeichert, dann gedruckt.
Ein leerer Papierhalter ist damit ein Papierproblem und kein Datenproblem — der
Vorgang steht in der Historie und lässt sich von dort nachdrucken. Die
Oberfläche sagt in dem Fall ausdrücklich, dass der Bon *nicht* gedruckt wurde.
Kopf- und Fußzeilen des Bons stehen in der Datenbank und werden unter
**Einstellungen** gepflegt, ebenso der Schalter „Bon beim Beenden automatisch
drucken". Der Probedruck dort beantwortet die Frage, ob es am Drucker oder an
der App liegt.
**Der Pi hängt per WLAN und DHCP im Netz.** Wechselt seine Adresse, druckt die
App nicht mehr, bis `PRINTER_HOST` angepasst ist — eine DHCP-Reservierung im
Router erspart das.
### Backup
Alles Wichtige steckt in **einer Datei**: `data/timer.db`.
@@ -275,7 +319,10 @@ timer-app/
├── DEPLOY.md ← dieses Dokument
├── README.md ← Kurzanleitung für Anwender
├── backend/
│ ├── server.js ← die gesamte Server-Logik (~470 Zeilen)
│ ├── server.js ← die gesamte Server-Logik
│ ├── escpos.js ← ESC/POS-Bausteine für den Bondrucker
│ ├── receipt.js ← Layout des Bons
│ ├── printer.js ← Aufruf der Brücke auf dem Pi
│ ├── package.json
│ ├── package-lock.json
│ ├── .env.example
@@ -288,7 +335,12 @@ timer-app/
└── deploy/
├── timer-app.service
├── Caddyfile.example
── nginx.conf.example
── nginx.conf.example
└── bondrucker/ ← Druckdienst für den Raspberry Pi
├── README.md ← Einrichtung des Pi
├── print-server.py ← der Dienst (nur Python-Standardbibliothek)
├── bondrucker.service
└── install.sh
```
`server.js` ist in beschriftete Abschnitte gegliedert (Konfiguration, Datenbank,
@@ -357,12 +409,16 @@ Alle Endpunkte außer `/health`, `/api/login`, `/api/auth/status` und
| `PATCH` | `/api/timers/:id` | `{customerName}` umbenennen |
| `POST` | `/api/timers/:id/pause` | anhalten |
| `POST` | `/api/timers/:id/resume` | fortsetzen |
| `POST` | `/api/timers/:id/stop` | beenden **und** in Historie speichern |
| `POST` | `/api/timers/:id/stop` | beenden **und** in Historie speichern; druckt den Bon, sofern eingerichtet. Optional `{print: bool}` überstimmt die Einstellung |
| `POST` | `/api/timers/:id/receipt` | Zwischenbon drucken, Timer bleibt unangetastet |
| `DELETE` | `/api/timers/:id` | verwerfen, **nichts** speichern |
| `GET` | `/api/settings` | Tarif lesen |
| `PUT` | `/api/settings` | `{rateAmount, rateInterval}` |
| `GET` | `/api/settings` | Tarif, Bon-Texte und Druckerzustand lesen |
| `PUT` | `/api/settings` | `{rateAmount, rateInterval}` sowie optional `{receiptHeader, receiptFooter, printAutoOnStop}` |
| `GET` | `/api/printer/status` | Erreichbarkeit und Zustand des Druckers |
| `POST` | `/api/printer/test` | Probedruck |
| `GET` | `/api/sessions` | Historie; Parameter `from`, `to`, `q`, `limit`, `offset` |
| `DELETE` | `/api/sessions/:id` | Eintrag löschen |
| `POST` | `/api/sessions/:id/receipt` | Bon aus der Historie nachdrucken |
| `GET` | `/api/sessions/export.csv` | CSV; gleiche Filterparameter |
| `GET` | `/health` | Status, ohne Anmeldung |