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:
co-authored by
Claude Opus 5
parent
5e83999950
commit
70756f3762
@@ -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 |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user