- JavaScript 96.3%
- Python 2%
- CSS 0.8%
- HTML 0.4%
- Shell 0.3%
- Other 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Repo-Hygiene-Runde: erst suchen und belegen, dann löschen. Vier parallele
Prüfungen (tote Exporte, i18n-Katalog, Frontend/CSS, Doku-Drift), jede Löschung
mit Referenznachweis — nichts geraten.
Gelöscht (Code)
- 30 tote Exporte in 18 src-Dateien (module.exports-Zeilen; die internen
Definitionen bleiben). Drei Kandidaten wurden als NICHT tot belegt und
behalten: winAnsi und safeEqual (echte Nutzung — der Alt-Report war hier
veraltet), pushToAll (in API.md dokumentiert).
- 33 tote i18n-Keys (37 Zeilen) — der advisory Report ist jetzt bei 0;
inkl. der 6 unauffaelligen Kandidaten, die nur als JS-Identifier/API-Feld
existieren (roleLabel, running, sessionActive, sold, tipGeneral, todaySales).
- 1 totes Global (doGuestCardHosted, seit
|
||
| .forgejo | ||
| docs | ||
| public | ||
| scripts | ||
| src | ||
| tests | ||
| types | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .prettierrc | ||
| AGENTS.md | ||
| API.md | ||
| backup_helper.py | ||
| CHANGELOG.md | ||
| compose.build.yaml | ||
| compose.yaml | ||
| docker-entrypoint.sh | ||
| Dockerfile | ||
| eslint.config.js | ||
| KNOWN_ISSUES.md | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| server.js | ||
| start.bat | ||
| start.sh | ||
| tsconfig.checkjs.backend.json | ||
| tsconfig.checkjs.base.json | ||
| tsconfig.checkjs.browser-base.json | ||
| tsconfig.checkjs.frontend-portal.json | ||
| tsconfig.checkjs.frontend-pos.json | ||
| tsconfig.checkjs.frontend-setup.json | ||
| tsconfig.checkjs.frontend-voucher.json | ||
| tsconfig.checkjs.tests.json | ||
BarSystem v2 — Mitgliederkonto POS
Point-of-Sale-System für Vereinsbars mit Mitgliederkonten, Guthabenverwaltung und Kartenzahlung via SumUp (Hosted Checkout + Solo-Terminal). Deutsche UI, kein Build-Schritt, ein Prozess, eine SQLite-Datei.
Auf einen Blick:
- 🍺 Mitglieder-Guthaben am POS abbuchen, aufladen (Bar/Karte/IBAN), Limits je Rolle
- 💳 Gastkauf: Bar, SumUp-Karte oder Gutschein — auch gemischt (Gutschein + Rest)
- 🎫 Digitale Gutscheine mit Barcode-Scan am POS und Saldo-Check im Portal
- 📊 Berichte: Umsatz-Charts, Z-Bericht, Trinkgeld/Fuxenkassa, Marge, Inventur
- 🔒 Verschlüsselte Backups — lokal + optional VPS (SCP) und/oder S3
- 🚀 Start:
./start.shoderdocker compose -f compose.yaml -f compose.build.yaml up -d --build→ Setup-Wizard
Neuerungen je Datum: CHANGELOG.md
Inhaltsverzeichnis
- Features
- Architektur & Projektstruktur
- Technologie-Stack
- Schnellstart
- Konfiguration
- Rollen & Berechtigungen
- Feature-Flags
- SumUp Kartenzahlung
- Offline-Modus (PWA)
- Sicherheitsmodell
- Design-Notizen
- Betrieb
- Umgebungsvariablen
- Docker
- API
- Entwicklung
- Bekannte offene Punkte
- Changelog
Features
Kasse (POS)
- Produktraster nach Kategorien, Drag-and-Drop-Sortierung, Keg-/Fass-Anzeige (Fass-Zustand direkt am Produkt), Sucher + Mitgliedersuche parallel.
- Warenkorb mit Mengen-Eingabe, Artikel entfernen, Wechselgeld-Rechner (abschaltbar), Gesamtsumme.
- Tastaturkürzel:
1–9= Produkt der aktiven Kategorie,Enter= Kaufen,Esc= Warenkorb leeren,Backspace= letztes Produkt entfernen. - Gastkauf getrennt vom Mitgliederkauf — parallele Bedienung ohne Mitgliedsauswahl möglich.
- Preisprofile: Standard / Happy Hour / Party — ein Klick schaltet alle Preise um; pro Produkt sind Overrides (Multiplikator oder Fixpreis) möglich, inkl. „Alle"-Override pro Profil.
- Gutschein-Zahlung: Wert-Gutschein als Zahlart — deckt der Gutschein den Warenkorb nicht vollständig, wird der Rest bar oder per SumUp-Karte in einer Buchung bezahlt (Split-Zahlung). Saldo prüfen zeigt Restwert/Status auch ohne Warenkorb; Barcode-Scanner (USB/Laser) lesen den Code aus dem Gutschein-PDF oder vom Bildschirm (Code-128).
- Mitglieder-Topup direkt am POS (Bar / Karte / IBAN-Überweisung).
Mitgliederkonten
- Guthabenverwaltung mit Kaufabzug und Untergrenzen (Floor-Kette, D50/v104/v105):
ALTER_HERR(Alter Herr): unbegrenzt (kein Limit)AKTIVER(Aktiver): bis −50 € (vorab erlaubt, wird beim Aufladen ausgeglichen)FREIBIER(Funktionskonto): 0 € — ein Budget, kein Kredit- Über der Rollentabelle stehen: Konto-Untergrenze (
members.balance_floor) > Gruppen-Kreditlimit (member_groups.credit_floor) > globalesfunction_account_floor(nur FREIBIER) > Rolle. Maßgeblich ist immereffectiveBalanceFloor(member)(src/db/pricing.js).
- Die Untergrenze wird auf jedem saldosenkenden Pfad erzwungen (Kauf, Retro, Vorbestellung, Gast-Deckel-Umwandlung, Topup-Storno, Überweisung, Freibier-Modus) — serverseitig, nicht nur in der UI. Ausnahme: Kneipen-Strafen (erzwungene Abbuchung, D63).
- Low-Balance-Alert: Mitglieder unterhalb des Limits werden gekennzeichnet; E-Mail-Benachrichtigung (einmalig pro Aufladung, Claim-vor-Send gegen Duplikate).
- NFC-Karten: Karte an den Leser halten → Mitglied wird ausgewählt (Karten-UID wird gehasht gespeichert, Kartenzuweisung ist Eindeutigkeits- geschützt: 409 bei bereits vergebener Karte).
- P2P-Überweisung + Geldanfragen (Flags
p2pTransfer/paymentRequests): Mitglieder überweisen Guthaben an andere Mitglieder oder bitten sie um einen Betrag. Die Anfrage selbst bewegt kein Geld — bezahlt wird über denselben Transfer-Pfad (identische Limits, identischer Ledger). Spam-Schutz serverseitig: 20 Anfragen/Tag, 3/Tag an dieselbe Person, max. 5 offene bzw. 100 € offen, 12 h Ruhe nach einer Ablehnung, Verfall nach 7 Tagen.
Aufladung
| Weg | Wo | Mechanik |
|---|---|---|
| Bar | POS | Direkte Gutschrift; 5s-Idempotenzfenster (gleicher Betrag pro Mitglied → 409) gegen Doppel-Tippen |
| Karte (SumUp) | POS + Portal | Hosted-Checkout-URL (Browser/Terminal) oder Cloud-API-Push ans Solo-Terminal; Verifikation serverseitig mit Betrag + Referenz |
| IBAN | POS + Portal | EPC-QR-Code (Konto/BIC/Name aus Env), Pending-Eintrag mit atomarem Claim; Admin bestätigt nach Geldeingang |
- Jede Kartenaufladung wird über
pending_topupsgeführt und erst nach serverseitiger SumUp-Verifikation gutgeschrieben (Betrag +checkout_refmüssen stimmen). Replay-Schutz: UNIQUE-Index aufsumup_checkout_id→ eine bezahlte checkout_id kann nie doppelt gebucht werden.
Storno / Undo
- Voll-Storno: Original-Transaktion →
REFUNDED, UNDO-Gegenbuchung wird erzeugt, Saldo korrigiert. - Teil-Storno (einzelne Position): Originalbetrag und ursprünglicher Saldo
bleiben unverändert. Die Position erhält einen
refunded_at-Marker und eine datierte UNDO-Ledger-Zeile dokumentiert den Teilbetrag. - Kartenzahlungen: Void refundet IMMER zuerst bei SumUp (fail-closed —
ohne gespeicherte
sumup_transaction_idwird abgelehnt), Teil-Stornos nutzen den SumUp-Teilrefund (POST /transactions/{id}/refundmit Betrag).
Sessions & Schichten
- Sessions (Veranstaltungen): Titel + verantwortliche Mitglieder (Approvers), Start/Ende, Session-Zeitplan.
- Schichten (Shifts): Öffnen/Schließen mit optionalem Kassenbestand
(Feature-Flag
requireCashCount, siehe unten), Kassen-Erwartungswert (Anfangsbestand + Verkäufe − Ausgaben), Über/Unter-Zählung, Übergabe-Notiz. Auto-Close (Session-Ende ohne Zählung) persistiert den Erwartungswert und lässt die Zählung für eine spätere Abrechnung offen (Reconciliation).
Transaktionen & Berichte
- Transaktionsliste: durchsuchbar (Name, Vulgo, E-Mail), filterbar (Typ, Zeitraum), Einzelpositionen aufklappbar, paginiert („Seite X von Y (Z Einträge)"), Export-freundlich.
- Umsatz-Bericht: Gast/Mitglied-Trennung, Balkendiagramme (responsive, Dark/Light, Hover-Tooltip mit Menge + Umsatz), Mitglieder-Vergleich, Zeitraum-Filter.
- Z-Bericht (Tagesabschluss): Bar-/Karten-Umsatz, Gastanzahl, Aufladungen, Session-/Shift-Zuordnung — inkl. Drawer-Soll/Ist.
- Trinkgeld & Fuxenkassa (Feature
tipScreen): Rückgeld als Trinkgeld verbuchen — Topf Fuxenkassa (Füxe) oder Bar; Saldo, Auszahlung, eigene Z-Bericht-Zeilen; Auszahlungen belasten die erwartete Kasse (Z). - Marge-Report: Umsatz vs. Wareneinsatz (COGS) je Produkt aus dem Inventar-Snapshot (historisch korrekt), Mitglieder/Gäste getrennt, Schwund-Kosten separat.
- Bon als PDF/QR: digitaler Kassenbon (80 mm) mit QR-Code — Drucken (window.print + Druck-CSS) oder QR-Scan für den Gast (Token-URL, HMAC-geschützt, rate-limited).
- Inventur-Timeline (wenn Inventar aktiv): Fass-Entnahme-Verlauf als Chart + Ein-/Ausblenden einzelner Produkte.
Verwaltung (Admin-UI)
- Mitglieder: CRUD, CSV-Import, CSV-Export (RFC 4180, Excel-tauglich, inkl. DSGVO-Einzelauskunft und Filter „nur negative Salden"), kombinierbare Stammdatenfilter für Rolle, Saldo, Karte, E-Mail und eigenen Krug sowie Sortierung „Schulden zuerst", Soft-Delete (nur bei Saldo 0), E-Mail-Suche nur mit entsprechender Permission (kein E-Mail-Orakel fürs Personal), NFC-Karten zuweisen/entfernen, PIN-Reset (mit E-Mail-Versand).
- Kategorien / Produkte / Preise: Inline-Bearbeitung, Drag-and-Drop-Reihenfolge.
- Preisprofile: Multiplikator-/Fixpreis-Overrides je Produkt, aktives Profil — plus Zeitfenster-Autoaktivierung („Happy Hour": von/bis, Wochentage; kleinste id gewinnt bei Konflikt; manuelle Wechsel werden beim nächsten Takt überschrieben).
- Inventar: Bestand pro Produkt, Fass-/Einheiten-Tracking (
/inventory/:id/tapöffnet neue Einheit), automatische Bestandsreduktion beim Kauf, Keg-Wechsel- Logik (leere Einheit → nächste öffnen), Warnschwellen + E-Mail-Alert (gedrosselt: nur bei Erfolg verbraucht). - Mitgliedsbeiträge (Fees): Status Offen/Bezahlt, Mahnung, Liste mit letzter Mahnungs-E-Mail.
- Retro-Buchung: Nachträgliche Käufe/Topups für eine vergangene Session
(benötigt Feature
retroBooking). - Backups: verschlüsselt (AES-256-GCM,
BACKUP_ENCRYPTION_KEY), manuell + automatisch (Serverstart, nach jedem Kauf, bei Session-Ende sowie 5-min-Snapshots), Download (verschlüsselte PC-Kopie), Upload, Transfer auf VPS via SFTP/paramiko (strikte known_hosts-Prüfung) und/oder S3-Objekt-Storage (AWS/MinIO/Wasabi/B2) — lokal bleibt immer erhalten; die Backup-Liste zeigt, wo jede Kopie liegt. - E-Mail-Texte: alle automatischen Mails als editierbare Templates mit
Platzhalter-Übersicht; Namensformat für
{name}wählbar (vollständig / Vulgo / Vorname / Nachname) — gilt auch für die Überweisungs-Referenz. - Portal: Erstzugang-Flow — Mitglieder ohne PIN erhalten eine Willkommens-Mail („Erstzugang: Noch keinen PIN?"), alle anderen den Reset-Flow; Antwort bleibt generisch (kein E-Mail-Enumeration-Orakel). Eigene E-Mail-Adresse änderbar (PIN + Bestätigungs-Code, Info-Mail an die alte Adresse).
- Kontoimporte (Admin): Kontoauszüge als camt.053/MT940/CSV hochladen, automatische Matching-Vorschläge (QR-Referenz/Namensnormalisierung — nie automatisch buchen), Gutschriften als TOPUP_IBAN buchen oder ablehnen; Re-Importe werden erkannt (idempotent).
- Gutscheine: digitale, übertragbare Gutscheine (Wert- oder Produkt-Typ, Ablaufdatum) — Code = Geheimnis, nur Hash gespeichert; Erstellen am POS (Code einmalig + PDF/QR/Barcode), Saldo-/Status-Check überall; Einlösung am POS (Produkt direkt oder als Zahlart, inkl. Split-Zahlung), Admin-Tab mit Liste + Ungültig-machen (Admin-Ausweg, nicht erstattbar); Z-Bericht mit eigener Zeile.
- Audit-Log: wer hat wann was getan (90-Tage-Rotation), filterbar, „Alle anzeigen"-Reset.
- Einstellungen: Feature-Flags, Primärfarbe, Tastenkürzel-Zuordnung, Daten-Reset (transactions / audit-log / members / events / alles).
Mitgliederportal (public/portal.html)
- Login mit PIN, Guthabenanzeige, Aufladen per Karte (SumUp Hosted) und IBAN (QR), Transaktionsverlauf, Pending-Topups-Status, Gutschein-Saldo per Code abfragen, Passwort/PIN-Reset (E-Mail-Code, brute-force-geschützt, 5 Versuche).
- Überweisen (Flag
p2pTransfer) und Geldanfragen (FlagpaymentRequests): Anfragen stellen, Posteingang mit Bezahlen/Ablehnen, Ausgang mit Zurückziehen, Badge am Tab, Push-Benachrichtigung (sofern VAPID konfiguriert). Details zur Richtung und zum Spam-Schutz:docs/DECISIONS.md(D47).
Bedienkomfort
- Komplett deutsche UI, Dark- + Light-Mode, primäre Akzentfarbe einstellbar.
- Digitale Kassenbons (Feature-Flag) nach dem Kauf.
- Kein Frontend-Build, kein Framework — Vanilla-JS-SPA, sofort ladbar.
- Barcode-Scanner (optional, beliebiger USB-HID-Scanner): Der Scanner tippt
die EAN wie eine Tastatur — einmalig im Admin (Verwaltung → Produkte → Produkt
bearbeiten → Feld „Barcode/EAN") auf die Flasche scannen, ab dann landet das
Produkt beim Scan direkt im POS-Warenkorb (preisprofil-gerecht). Kein Flag,
keine externe Datenbank: Die
products-Tabelle IST die Zuordnung. Doppelte Zuweisung wird vom Server abgelehnt (409). Funktioniert neben NFC-Karten- und Gutschein-Scans (reine Ziffern-Bursts werden als EAN erkannt).
Architektur & Projektstruktur
barsystem-v2/
├── server.js # Express-Launcher: Helmet/CSP, Rate-Limits, Routen, Statics
├── src/
│ ├── config.js # Umgebungsvariablen (SumUp, SMTP, Limits, …)
│ ├── db.js # Re-Exporte: db, getActiveProfile, getDisplayPrice,
│ │ # BALANCE_LIMITS, getOr404, getActiveEvent
│ ├── middleware.js # Auth, Permissions, Rate-Limiter (Staff vs Portal getrennt),
│ │ # requireFeature, requireSumupEnabled, catchAsync
│ ├── permissions.js # Einzige Quelle: ALL_PERMISSIONS (10), Rollen-Defaults
│ ├── money.js # parseMoney / cents (JEDER Betrag geht hier durch)
│ ├── utils.js # assertString, assertPositiveAmount, escapeLike,
│ │ # parseCSV, generateEpcQr, genToken
│ ├── dateUtils.js # lokale Tagesgrenzen (BUSINESS_TIMEZONE), UTC-Format
│ ├── utils_crypto.js # PBKDF2-Hashing, AES-256-GCM-Verschlüsselung
│ ├── db/
│ │ ├── schema.js # Kanonisches FINAL-Schema
│ │ ├── migrations.js # Runner: lädt Module aus db/migrations/, v45+
│ │ ├── seed.js # Erst-Bootstrap (Admin/Manager/Staff, Basisprodukte)
│ │ ├── pricing.js # BALANCE_LIMITS je Rolle
│ │ └── connection.js
│ ├── routes/ # HTTP-Schicht: Validierung, Permissions, JSON
│ │ ├── auth.js, users.js, members.js (+ members/crud.js), catalog/ (products,
│ │ │ categories, inventory, bundles, validation), checkout.js # /checkout,
│ │ │ # /checkout/guest(+/init), /checkout/undo-item
│ │ ├── topups.js # cash/card(+mode=terminal)/iban, pending-bestätigen
│ │ ├── terminal.js # SumUp-Cloud-API: list/pay/status/cancel/pair + Webhook
│ │ ├── portal/ # Portal-Realm: security.js, topups.js, transfers.js,
│ │ │ # requests.js (Geldanfragen v100) + recipients.js
│ │ │ # (geteilte Empfänger-Auflösung), preorders.js,
│ │ │ # schedule.js, vouchers.js,
│ │ │ # emailChange.js, orders.js …
│ │ │ # (penalties.js liegt unter members/ — s. u.)
│ │ ├── vouchers.js # Gutschein-CRUD + PDF/QR/Barcode (WP8)
│ │ ├── bankImport.js # camt.053/MT940/CSV-Import (Review-Queue)
│ │ ├── stream.js # SSE-Live-Events (WP-O3)
│ │ ├── guestTabs.js # Gast-Deckel/Kredit-Tabs (v79)
│ │ ├── posOrders.js # Portal-Bestellungen POS-Management (v92)
│ │ ├── apiKeys.js # X-API-Key-Auth (NF-5)
│ │ ├── suppliers.js, purchaseOrders.js # Lieferanten + Bestellungen (NF-7)
│ │ ├── events.js, shifts.js, reports.js, zReport.js, features.js,
│ │ ├── settings.js, backups.js, auditLog.js, reset.js, fees.js,
│ │ ├── profiles.js, health.js, maintenance.js, push.js, exports.js
│ ├── services/ # Geschäftslogik + ALLE DB-Schreibvorgänge in transactions
│ │ ├── checkoutService.js# Rest-Datei: reconcilePendingRefundOperations (kein Barrel mehr, B3)
│ │ ├── purchaseService.js, undoService.js, retroService.js, checkoutHelpers.js
│ │ ├── memberService.js (+ members/: topups, transfers, moneyRequests, penalties),
│ │ ├── sumupService.js (+ sumup/), mailService.js,
│ │ ├── stockAlertService.js, featureFlags.js, shiftService.js, auditLog.js, logger.js,
│ │ ├── guestTabService.js, bundleService.js, orderService.js, preorderService.js,
│ │ ├── feeService.js (+ fees/), reportService.js, eventService.js,
│ │ ├── backupWatchdog.js, profileScheduler.js, csvExport.js, pdfReceipt.js,
│ │ ├── purchaseOrderService.js, supplierService.js, pushService.js,
│ │ ├── voucherService.js, crypto.js, liveEvents.js
│ └── db.js
├── public/
│ ├── index.html # Kassen- + Admin-SPA (lädt alle Module)
│ ├── portal.html # Mitgliederportal
│ ├── manifest.json, sw.js # POS-PWA
│ ├── portal-manifest.json, portal-sw.js # Mitgliederportal-PWA
│ ├── css/style.css
│ └── js/ # Vanilla-Module (prettier-formatiert)
│ ├── escape.js # esc()/dataArgs() — XSS-kritisch, lädt zuerst
│ ├── i18n.js # Deutscher UI-Copy-Katalog T + t() (dependency-free)
│ ├── error-catcher.js # globaler JS-Fehler-Toast (M4: externalisiert)
│ ├── api.js # API-Client (Cookie/CSRF; Bearer-Fallback)
│ ├── offline-db.js, offline-queue.js # Offline-Modus: IndexedDB + Sync (WP-O1)
│ ├── live.js # SSE-Client (WP-O3)
│ ├── app.js # geteilte Helfer (localSqliteDate, isPOSRoute,
│ │ # balanceClass/roleClass …)
│ ├── app-actions.js # Action-Dispatcher (invokeAction + Delegation, M5-Split)
│ ├── app-shell.js, app-auth.js, app-router.js # Shell, Login, Routing (M5-Splits)
│ ├── toast.js # Toast-API (lädt früh)
│ ├── pos.js # Kasse: State, Rendering, Mitgliederauswahl
│ ├── pos-catalog.js # Kategorie-/Produkt-Grid + Profilwahl (aus pos.js)
│ ├── pos-freedrink.js # Freibier-Modus (freeDrinkState)
│ ├── pos-guest-tabs.js, pos-guest-tabs-settle.js # Gast-Deckel + Begleichen
│ ├── pos-orders.js # Portal-Bestellungen im POS (v92)
│ ├── scanner-camera.js # Kamera-Barcode-Scan (NF-4)
│ ├── push-client.js # Web-Push-Subscription (NF-2, POS + Portal)
│ ├── pos-session.js # Sessions/Shifts/Kassenbestand/Verantwortliche
│ ├── pos-cart.js # Warenkorb (addToCart/changeQty/drawCart …)
│ ├── pos-payment-core.js # Payment-State, Contexts, Cancellation, Terminal-Scheduler
│ ├── pos-payment-card.js # Terminal-Checkout auf dem Core
│ ├── pos-tips.js # Trinkgeld/Auszahlungen (WP1); pos-vouchers.js: Gutscheine (WP8)
│ ├── pos-payment.js # Bezahlung (Guest Cash/Card, Member-Checkout)
│ ├── pos-topup.js # Aufladen (Cash/Card/IBAN, Topup-Terminal-Polling)
│ ├── pos-barcode.js # Barcode/EAN-Eingabe (WP-O4)
│ ├── pos-operations.js # Inventar-Tap, Undo-Polling, NFC, Tastatur
│ ├── admin*.js # Admin-Tabs (je Domain eine Datei: members/categories/
│ │ # products/profiles/users/events/transactions/…)
│ ├── reports-chart.js, reports-sales.js, reports-z.js, reports-inventory.js,
│ │ # reports-margin.js, reports-tips.js, reports-hook.js
│ ├── admin-settings-sections.js # Einstellungen: SumUp/Server/VPS/Audit
│ ├── admin-maintenance.js # Wartung: Migrationszustand + Neustart
│ ├── setup.js # First-Setup-Wizard (setup.html)
│ ├── portal-core.js, portal-auth.js, portal-tabs.js, portal-pwa.js
│ │ # Mitgliederportal (M5-Split, portal.html)
│ └── date.js, escape.js s.o.
├── tests/ # Jest: Unit- + Integrationstests (2888)
├── Dockerfile, compose.yaml (Produktion: Registry-Pull), compose.build.yaml
├── AGENTS.md # Agent-Guide (Architektur, Invarianten, Pitfalls)
├── API.md # API-Referenz
└── KNOWN_ISSUES.md # Einzige lebende Befundliste
Schichtung (wichtig für Änderungen): Routen validieren und antworten,
Services tragen die Geschäftslogik und schreiben die DB ausschließlich in
db.transaction(). Routen importieren die Services direkt aus ihrem
Heimatmodul (kein checkoutService-Barrel mehr, B3).
Technologie-Stack
- Backend: Node.js 22.14+ oder 24+ (better-sqlite3 v13 segfaultet auf 22.13), Express, better-sqlite3 v13 (synchron — keine Await-Races innerhalb von Transaktionen)
- Frontend: Vanilla JS SPA, kein React/Vue, kein TypeScript, kein Build
- Auth: httpOnly-Cookie-Sessions (
session/portal_session, SameSite=Strict, Secure hinter HTTPS) + realm-getrennte Double-Submit-CSRF-Cookies (csrf/portal_csrf); PBKDF2-SHA-512 (600k Iterationen). Bearer-Header nur noch als Übergangs-Fallback (Token-Fixtures, Alt-Clients) - Tests: Jest (in-memory SQLite, Savepoint-Isolation pro Test, KEIN
resetModules()pro Test) - Deployment: ein Prozess, eine SQLite-Datei, optional Docker
Schnellstart
Voraussetzungen
- Node.js 22.14+ oder 24+ (better-sqlite3 v13 liefert NAPI-Prebuilds im
npm-Tarball; 22.13 crasht beim
require)
Docker (empfohlen)
cp .env.example .env
# Im Container optional: fehlende Secrets (Backup-Key, Setup-Token, VAPID)
# erzeugt der Start selbst und schreibt sie in die Logs (data/secrets.env).
# Für headless-Deployments ohne Wizard stattdessen:
# INITIAL_ADMIN_PASSWORD=<starkes Passwort>
# BACKUP_ENCRYPTION_KEY=<32-Byte-Schlüssel>
# compose.yaml ist pull-only (Produktion zieht das CI-Image aus der Registry);
# für den lokalen Build den Override mitgeben:
docker compose -f compose.yaml -f compose.build.yaml up -d --build
Im Container erzeugt der Start fehlende Secrets (BACKUP_ENCRYPTION_KEY,
SETUP_TOKEN, VAPID-Paar) selbst, legt sie unter data/secrets.env (0600) ab
und schreibt sie in die Logs — docker compose logs app | grep SETUP_TOKEN
liefert das Token für /setup.html. Diese Datei nie löschen: ohne
BACKUP_ENCRYPTION_KEY sind alte Backups unlesbar.
Der Datenordner ./data/ wird als Bind-Mount gemountet (./data:/app/data).
Compose bindet standardmäßig nur an 127.0.0.1; BIND_ADDRESS=0.0.0.0 nur
hinter einem TLS-Reverse-Proxy verwenden.
Linux (manuell)
cd barsystem-v2
npm install
REQUIRE_HTTPS=false PORT=3000 node server.js # erster Start: /setup.html im Browser
Einfacher: ./start.sh (installiert Dependencies + Python-venv mit
paramiko/boto3 bei Bedarf, prüft ausstehende Migrationen, startet mit
REQUIRE_HTTPS=false für lokale Entwicklung). Windows: start.bat. Beim ersten
Start (leere DB) erscheint automatisch /setup.html — kein
INITIAL_ADMIN_PASSWORD nötig.
REQUIRE_HTTPS=false ist ausschließlich für lokale Entwicklung gedacht
(Default: true → HTTP-Anfragen werden mit 426 abgelehnt).
Erster Start — Setup-Wizard
- Die DB wird automatisch angelegt (Migrationen v1–v97).
- Ohne
INITIAL_*_PASSWORD-env bleibt die Benutzer-Tabelle leer — beim Öffnen der App im Browser erscheint automatisch/setup.html(der First-Setup-Wizard): dort Admin-Benutzername + Passwort wählen, optional SMTP/Bank/Zeitzone eintragen. Der Wizard legt den Admin-User an und setztseed_done. - Mit dem neuen Admin einloggen, Feature-Flags prüfen (Admin → Einstellungen), SumUp-Credentials setzen (siehe SumUp).
INITIAL_ADMIN_PASSWORDin.envist seit dem Setup-Wizard optional — nur noch für headless/Docker-Bootstrap nötig (dann wird der Login wie gehabt mit erzwungenem Passwortwechsel gebootstrapt).
Konfiguration
Es gibt drei Konfigurations-Ebenen — die meisten Werte liegen in .env,
einige werden in der Admin-UI gesetzt, SumUp-Credentials entstehen im
SumUp-Merchant-Portal:
| Was | Wo | Wann nötig |
|---|---|---|
| Backup-Key (env), optionale Bootstrap-Passwörter, SumUp, SMTP, IBAN, Ports | .env (Kopie von .env.example) |
im Container optional — Backup-Key/Setup-Token erzeugt der Start selbst (data/secrets.env + Log); Admin-Passwort kommt aus dem Wizard |
| Feature-Flags, Primärfarbe, Tastenkürzel, Bankdaten, Mitgliedsbeitrag, Reader-Pairing, SumUp/VPS/Server-Settings | Admin-UI → Einstellungen | nach dem ersten Login |
| SumUp API-Key / Merchant Code / Affiliate Key | me.sumup.com → Developer Settings / Toolkit |
nur bei Kartenzahlung |
| Solo-Reader-Pairing-Code | am SumUp-Solo-Gerät (Connections → API) | nur bei Terminalzahlung |
Schritt 1 — .env anlegen
cp .env.example .env
Zwingend in .env (ohne diesen Wert verweigert der Server Backups):
| Variable | Wert | Erzeugen |
|---|---|---|
BACKUP_ENCRYPTION_KEY |
32 Byte | openssl rand -hex 32 |
Im Container nicht mehr nötig: Docker-Start erzeugt fehlende Secrets (
BACKUP_ENCRYPTION_KEY,SETUP_TOKEN, VAPID-Paar) selbst, legt sie unterdata/secrets.env(0600) ab und schreibt sie in die Logs (docker compose logs app) — siehedocs/DEPLOYMENT.md→ Weg 3.AUTO_SECRETS=0schaltet das ab.
Alles andere geht über den Setup-Wizard bzw. die Admin-UI — das
Admin-Passwort wird beim ersten Start im Browser gewählt (/setup.html),
SumUp/SMTP/Bank/Zeitzone/VPS lassen sich danach in den Einstellungen setzen
(DB-Werte haben Vorrang vor env). INITIAL_ADMIN_PASSWORD/INITIAL_MANAGER_*/
INITIAL_STAFF_* sind nur noch für headless/Docker-Bootstrap relevant.
Erster Start mit minimalem .env (Wizard übernimmt den Rest):
PORT=3000
DB_PATH=data/bar.db
REQUIRE_HTTPS=false # nur lokal; produktiv true (Default)
BACKUP_ENCRYPTION_KEY=$(openssl rand -hex 32)
# KEIN INITIAL_ADMIN_PASSWORD nötig — /setup.html erscheint automatisch
Schritt 2 — Setup-Wizard (erster Start)
- Server starten (Docker oder
node server.js). - App im Browser öffnen →
/setup.htmlerscheint automatisch (leere DB): Admin-Benutzername + Passwort wählen, optional SMTP/Bank/Zeitzone. - Danach einloggen; optional
INITIAL_MANAGER_PASSWORD/INITIAL_STAFF_PASSWORDin.envsetzen (oder Benutzer in der UI anlegen).
Schritt 3 — Admin-UI (Einstellungen)
Nach dem Login in der Admin-Ansicht:
- Funktionen (Feature-Flags): SumUp, Gastkauf, Karten-Aufladung, QR-Codes,
NFC-Karten, digitaler Bon, Tastaturkürzel, Wechselgeld-Rechner, Kassenbestand-
Pflicht (
requireCashCount), Inventar-Tracking, Retro-Buchung, Z-Bericht — hier an/aus schalten (siehe Tabelle im Abschnitt Feature-Flags). - Einstellungen: SMTP-Zugang + „Testmail senden", Admin-E-Mail,
Bankdaten (IBAN/BIC/Kontoinhaber — alternativ zu den
.env-Werten), Mitgliedsbeitrag-Betrag und -Periode. - Tastenkürzel: freie Zuordnung von Tasten auf Produkte.
- Darstellung: Primärfarbe, Dark/Light-Mode.
- Daten: Reset-Bereiche (transactions / audit-log / members / events / alles).
Schritt 4 — SumUp (nur bei Kartenzahlung)
- API-Key:
me.sumup.com→ Settings → Developer Settings (für Tests: eigene Sandbox anlegen) → API-Key →.env:SUMUP_API_KEY. - Merchant Code →
.env:SUMUP_MERCHANT_CODE. - Affiliate Key: Toolkit → Affiliate Keys, Application identifier z. B.
at.bar.vereinsbar→.env:SUMUP_AFFILIATE_KEY+SUMUP_APP_ID. - PUBLIC_URL setzen (Redirect-Ziel nach Hosted-Zahlung).
- Solo-Terminal (optional): Pairing-Code am Gerät →
POST /api/terminal/pair(Benutzer mitsettings-Permission). Details + Virtual-Solo-Test: SumUp Kartenzahlung.
Typische Szenarien
Vereinsbetrieb (Vertrauensbasis, keine Karten):
BACKUP_ENCRYPTION_KEY=$(openssl rand -hex 32)
# Admin-Passwort kommt aus dem Setup-Wizard (erster Start)
# SumUp-Zeilen auskommentiert lassen
→ Beim ersten Start /setup.html durchspielen; in der UI: requireCashCount
AUS (kein Kassenbestand nötig), Rest default.
Party (Kasse + Karten):
BACKUP_ENCRYPTION_KEY=$(openssl rand -hex 32)
SUMUP_API_KEY=sk_live_…
SUMUP_MERCHANT_CODE=…
SUMUP_AFFILIATE_KEY=ak_…
SUMUP_APP_ID=at.bar.vereinsbar
PUBLIC_URL=https://barsystem.example.com
→ Beim ersten Start /setup.html; in der UI: requireCashCount AN
(Anfangs-/Endbestand Pflicht, Backend lehnt 0 ab), sumupEnabled AN, ggf.
Happy-Hour-/Party-Preisprofil anlegen und aktivieren, Personal-Account anlegen
(Rolle STAFF = „Personal").
E-Mail-Benachrichtigungen (Low-Balance-Alerts, PIN-Reset, Fee-Mahnungen):
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=user@gmail.com
SMTP_PASS=<App-Passwort>
SMTP_FROM=user@gmail.com
ADMIN_EMAIL=admin@example.com
Backup auf VPS (automatisch bei Start, nach Käufen und Sessions — lokal max. 1/5 min, Upload zu VPS/S3 1× täglich via UPLOAD_INTERVAL_HOURS):
VPS_HOST=your-vps.example.com
VPS_USER=root
VPS_PASS=…
REMOTE_DIR=/root/barsystem-backups/
VPS_KNOWN_HOSTS=/pfad/known_hosts # strikte Prüfung immer aktiv
Ohne VPS_HOST bleiben Backups lokal unter data/backups/.
Alle Variablen im Detail: Umgebungsvariablen.
Rollen & Berechtigungen
| Rolle | Beschreibung |
|---|---|
| ADMIN | Alles (inkl. Rollenvergabe, Einstellungen, Backups) |
| MANAGER | Betrieb + Berichte + Mitglieder-/Produktverwaltung |
| STAFF | Nur Kasse (POS) — kein Admin, keine Berichte |
Es gibt 10 Berechtigungen (src/permissions.js ist die einzige Quelle):
pos, members, products, inventory, reports, users, settings,
manageFeatures, backups, features-Zugriff u. ä. — nur ADMIN darf die
ADMIN-Rolle oder elevate Permissions vergeben (serverseitig erzwungen).
UI-Anzeige: Rolle STAFF erscheint als „Personal", interne Werte bleiben
englisch.
➡️ Anleitungen zum Ausdrucken (deutsch, mit Screenshots und roten Markierungen):
| Anleitung | Vollversion | A4-Aushang (1 Seite, zum Aufhängen) |
|---|---|---|
| Bardienst / Kasse | Bardienst_Schnellstart.pdf |
Bardienst_Aushang_A4.pdf |
| Mitgliederportal | Mitgliederportal_Anleitung.pdf |
Mitgliederportal_Aushang_A4.pdf |
| Verwaltung (Admin) | Verwaltung_Anleitung.pdf |
Verwaltung_Aushang_A4.pdf |
Dazu die Textfassung für Bardienst: docs/BARDIENST_QUICKSTART.md.
Neu bauen (nach UI-Änderungen): siehe scripts/docs/README.md.
Feature-Flags
Verwaltung: Admin → Einstellungen (Tab „Funktionen"). Aktuelle Flags
(Auszug — Quelle: FEATURE_FLAG_DEFINITIONS in src/constants.js, geseedet in
src/db/migrations.js; die UI-Keys dazu führt public/js/admin-features.js):
| Flag | Effekt |
|---|---|
sumupEnabled |
SumUp-Kartenzahlung aktiv (Routen sonst 503) |
guestCheckout |
Gastkauf erlaubt |
cardTopup |
Karten-Aufladung am POS durch Personal |
portalCardTopup |
Karten-Aufladung im Portal (Mitglieder, ohne Terminal) |
qrCodes |
IBAN-QR-Codes |
nfcCards |
NFC-Kartenleser |
digitalReceipt |
Digitaler Bon nach Kauf |
keyboardShortcuts |
Tastaturkürzel im POS |
disableChangeScreen |
Wechselgeld-Rechner ausblenden |
requireCashCount |
Anfangs-/Endbestand bei Session/Shift PFLICHT (Backend lehnt 0 ab) — für Vereine auf Vertrauensbasis aus, für Partys an |
inventoryTracking |
Bestand/Fass-Tracking |
retroBooking |
Retro-Buchung |
zReport |
Z-Bericht |
offlineMode |
Offline-Modus (PWA & Offline-Warteschlange) — siehe Offline-Modus (PWA) |
p2pTransfer |
P2P-Guthabenübertrag im Portal (Mitglied zu Mitglied) |
paymentRequests |
Geldanfragen im Portal (bitten statt überweisen; eigener Kill-Switch gegen Spam, v101) |
memberFees |
Mitgliedsbeiträge (Tab Verwaltung → Beiträge + alle /api/fees-Routen; aus = 503, v102) |
memberFeeAutoSend |
Beitrags-Automatik: Versand am Stichtag + Admin-Erinnerung 14 Tage vorher (aus = nur manueller Versand, v102) |
memberFeeEmails |
Beitrags-Mails überhaupt (Fälligkeits-Mail + Mahnung) |
cashCarryOver |
Kassenstand-Modus: Anfangsbestand wird automatisch vom letzten gezählten Endbestand übernommen (v103; Feld „Anfangsbestand" entfällt) |
guestPricing |
Zweiter Preis (Gastpreis) je Produkt für Gast-Verkäufe (v97) |
SumUp Kartenzahlung
Zwei Zahlungswege, beide über die SumUp-API (api.sumup.com/v0.1):
- Hosted Checkout (Portal + POS-Fallback): Der Server erzeugt einen
Checkout mit
hosted_checkout.enabled, die Zahlungsseite öffnet sich im Browser (hosted_checkout_url). Keine Hardware nötig. - Cloud API / Solo-Terminal (POS, physisches Terminal): Die Zahlung wird an einen verbundenen SumUp-Solo-Reader gepusht, der POS pollt den Status (alle 2 s) und bucht nach Zahlung über den verifizierten Pfad. Ohne Reader: automatischer Hosted-Fallback.
Purpose-Binding (Sicherheit)
Jeder server-seitig erzeugte Checkout trägt eine Referenz mit Zweck-Präfix:
guest- (Gastkauf), topup- (POS-Aufladung), portal-topup- (Portal).
Der Gastkauf-Pfad lehnt Topup-Referenzen ab (409) und umgekehrt — eine bezahlte
Aufladung kann nie als Gastkauf verbucht werden. Zusätzlich: UNIQUE-Index auf
sumup_checkout_id → Replay-Schutz (409 „bereits gebucht").
Einmalige Einrichtung (Solo-Terminal)
- Credentials:
me.sumup.com→ Settings → Developer Settings (Sandbox: eigene Sandbox anlegen) → API-Key (sk_live_…) →.env:SUMUP_API_KEY. - Merchant Code →
.env:SUMUP_MERCHANT_CODE. - Affiliate Key: Toolkit → Affiliate Keys, Application identifier z. B.
at.bar.vereinsbar→.env:SUMUP_AFFILIATE_KEY+SUMUP_APP_ID. - Solo einschalten → Connections → API → Connect → Pairing-Code.
- Pairing einmalig:
POST /api/terminal/pairmit{ "pairing_code": "…" }(Benutzer mitsettings-Permission). Der Reader wird ab dann automatisch überGET /api/terminal/listgefunden (erster Reader wird genutzt).
⚠️ Solo Lite unterstützt die Cloud API NICHT (nur Bluetooth-SDKs). Für den Terminalweg ist das volle Solo nötig (~150 €). Vor dem Kauf mit Virtual Solo (
virtual-solo.sumup.com) + Sandbox testen.
Zahlungsabläufe
- POS Gast-Kauf: Karte-Button → Reader vorhanden? → Terminal-Push + Polling
→ PAID → Buchung (
/checkout/guest). Kein Reader → Hosted-Fallback (window.open + „Zahlung abschließen"). - POS Mitglieder-Aufladung: Karte-Button → Terminal-Push (
mode=terminal) → Polling → Gutschrift. - Portal (remote): Hosted Checkout per URL/QR → „Zahlung prüfen".
- Webhook
/webhooks/sumup: verifiziert IMMER per API (nie dem Payload vertrauen) und bucht Topups automatisch gut. Gast-Checkouts bucht er nicht (kein Warenkorb-Kontext) — das macht der POS. - Storno: Void refundet zuerst bei SumUp (Voll- oder Teilrefund), dann lokal (fail-closed).
- Reconcile: Bezahlte Checkouts sind per Referenz-Präfix über die SumUp-API abfragbar.
Offline-Modus (PWA)
Der POS ist als PWA installierbar und kann ohne Internetverkauf weiterarbeiten
(Feature-Flag offlineMode, Default aus). Verkäufe werden lokal in einer
IndexedDB-Queue gepuffert und nach dem Wiederverbinden automatisch synchronisiert.
Auch das Mitgliederportal ist als eigene, separate PWA installierbar. Es nutzt
portal-manifest.json, startet bei /portal.html und hat einen eigenen Service Worker
mit getrenntem Cache. Der Portal-Service-Worker hält nur die Portal-Shell bereit; Login,
Guthaben, Aufladungen und alle übrigen Mitgliederdaten bleiben aus Sicherheitsgründen
online-only und werden nie im Cache gespeichert.
Installation (Android/Chrome)
- Die PWA benötigt einen sicheren Kontext — die App muss also über
HTTPS (oder
localhost) ausgeliefert werden, sonst registriert sich der Service Worker nicht.REQUIRE_HTTPS=falsereicht dafür nicht. - Menü → „Zum Startbildschirm hinzufügen" / „App installieren" — der Service Worker cached die App-Shell, danach startet das Tablet auch im Garten ohne Netz (Start fällt offline auf die gecachte Shell zurück).
- Portal:
/portal.htmlöffnen → Menü → „Zum Startbildschirm hinzufügen" / „App installieren". Die installierte App öffnet direkt das Mitgliederportal.
Wartung: Cache-Version
public/sw.jshat eineVERSION-Konstante (const VERSIONin der Datei selbst — die frühere Handzahl hier ist dreimal gedriftet, deshalb steht sie nicht mehr im README). Bei JEDEM Deployment mit JS/CSS-Änderungen hochzählen — das ist der manuelle Cache-Buster, sonst liefert der Service Worker alte Dateien aus. Alternativ auf dem Tablet in den App-Infos den Cache leeren.public/portal-sw.jshat einen eigenen Versionszähler (aktuellbs-portal-v11) und Cache-Namespace. POS- und Portal-PWA löschen beim Aktivieren nicht mehr die jeweils andere Cache-Version.
Offline-Queue
- Enqueue: Mitgliederkauf oder Gast-Barzahlung offline → Buchung landet in
der Queue (
clientCapturedAt= Kaufzeitpunkt,offlineBooking: true,request_ididempotent, Offline-Epoche für Debug/Transparenz). Warenkorb wird geleert, der Bon trägt den Hinweis „OFFLINE — noch nicht verbucht". - Sync: FIFO nach dem Wiederverbinden (online-Event, App-Start, 60-s-Takt,
manuell). 2xx → gebucht, 4xx → Fehlerliste (Banner), Netzwerkfehler/5xx →
Abbruch mit Wiederholung (Reihenfolge bleibt erhalten). Jede Sync-Buchung
trägt den Header
X-Offline-Booking: 1. - Anzeige: Banner rechts oben zeigt Queue-Status/Fehler; die Fehlerliste erlaubt „Erneut versuchen" und „Verwerfen".
Limit-Override (Layer-2-Politik)
- Das Tablet prüft das Rollen-Limit lokal gegen die projizierte Balance (Cache-Stand − offene Queue). Unter dem Limit wird offline verweigert (Gast-Barzahlung bleibt möglich).
- Weicht der Cache-Stand ab (zweites Terminal hat zwischenzeitlich belastet),
kann eine Offline-Buchung beim Sync unter das Limit fallen: Der Server
bucht sie trotzdem — das Bier ist verkauft, die Buchung darf nicht verloren
gehen — markiert sie aber mit
offline_limit_override(sichtbar in Protokoll/Z-Bericht und Audit). Einlösen/Aufladen gleicht das Konto aus.
Sicherheitsmodell
- Authentifizierung: httpOnly-Cookie-Sessions (
sessionfür Staff,portal_sessionfür das Mitgliederportal; SHA-256-gehashte Token in der DB, niemals Klartext). Token-Invalidierung bei Passwortwechsel/Logout. Der Bearer-Header bleibt als Übergangs-Fallback erhalten (Token-Fixtures, Alt-Clients), wird vom Frontend aber nicht mehr gespeichert. - CSRF: Double-Submit-Cookies, realm-getrennt (
csrf/portal_csrf), erzwungen percsrfGuardauf allen State-Changing-Pfaden, sobald eine Session-Cookie vorhanden ist (Bearer-only-Clients ohne Cookie sind ausgenommen); SameSite=Strict + Secure hinter HTTPS. Staff- und Portal-Cookies werden getrennt gesetzt/gelöscht — Logout in einem Realm lässt den anderen unberührt. - Content Security Policy (Helmet):
script-src 'self',script-src-attr 'none',object-src 'none',frame-ancestors 'none',base-uri 'self',upgrade-insecure-requests. Seit dem M4-Refactor existieren keinerlei Inline-JS-Handler und keine Inline-<script>-Blöcke mehr — sämtliche Events laufen überdata-action-Delegation (eine Funktion inapp-actions.js, Argumente als JSON indata-args, keineval). Tests erzwingen beides. - Rate-Limiting: getrennte Maps für Staff-API und Portal (IP + Account), Fenster 15 min; PIN-Reset zusätzlich DB-gestützt (5 Versuche).
- Geld: jeder Betrag durch
parseMoney; Rollen-Limits auf jedem saldosenkenden Pfad; Preisänderungen zwischen Verifikation und Buchung brechen den Kauf (M7). - Backups: AES-256-GCM-verschlüsselt; automatische Trigger (Serverstart,
Kauf, Session-Ende, 5-min-Snapshots); SFTP-Transfer (paramiko) mit strikter
known_hosts-Prüfung. - HTTPS:
REQUIRE_HTTPSlehnt HTTP ab (426);TRUSTED_PROXYvertrautX-Forwarded-*nur von den erkannten Proxys (Defaultloopback;auto= Loopback + eigene private Netze ist ein explizites Opt-in und wird voncompose.yamlgesetzt — beim Start loggentrust proxy-Zeilen den Satz).
Design-Notizen
- Zeitstempel werden in UTC gespeichert (
datetime('now'), FormatYYYY-MM-DD HH:MM:SS). Tagesberichte rechnen inBUSINESS_TIMEZONE(DefaultEurope/Berlin). Nie ISO-T-Strings in SQL-Vergleichen mitdatetime('now')mischen (Leerzeichen <T→ Same-Day-Brüche). - Partial-Undo: Teil-Stornos schreiben eine eigene UNDO-Ledger-Zeile („Storno … von Transaktion #x (Teil)"). Die Drawer-Berechnung berücksichtigt Original und Gegenbuchung; der ursprüngliche Umsatz bleibt als Bruttowert erhalten.
- Drawer (Kassen-Soll):
expected = opening_cash + Verkäufe − Ausgaben; Auto-Close ohne Zählung lässtclosing_cash/cash_over_shortNULL bis zur gezählten Abrechnung. - Migrationen: v1–v44 gesquasht, v45+ als Module in
src/db/migrations/mitup: (db) => …(Runner übergibtdb!). Neue Migrationen IMMER anhängen. - LLM-Freundlichkeit:
AGENTS.md(Architektur, Invarianten, Pitfalls), prettier-formatierter Code (npm run format), Tests formatierungs-agnostisch.
Betrieb
- Graceful Shutdown: SIGTERM → offene Verbindungen werden beendet, laufende Requests abgeschlossen.
- Backups: verschlüsselt, automatisch bei Serverstart, nach jedem Kauf und bei Session-Ende (+ 5-min-Snapshots) + manuell in der Admin-UI.
- API-Reset:
POST /api/reset(PermissionmanageFeatures) setzt ausgewählte Bereiche zurück (transactions / audit-log / members / events / alles) — mit Absicherung gegen versehentliches Auslösen. - Beenden: manuell
Ctrl+C(SIGTERM), Dockerdocker compose down.
Umgebungsvariablen
| Variable | Beschreibung | Default |
|---|---|---|
PORT |
Server-Port | 3000 |
DB_PATH |
SQLite-Dateipfad | data/bar.db (Docker: /app/data/bar.db) |
BUSINESS_TIMEZONE |
Zeitzone für Tagesberichte (Zeiten in UTC gespeichert) | Europe/Berlin |
TRUSTED_PROXY |
Boolean: X-Forwarded-Proto/Host von erkannten Proxys vertrauen | true |
TRUSTED_PROXY_IP |
Peers für X-Forwarded-*: loopback (Default), auto (Loopback + eigene private Netze, Opt-in), explizite Liste (kommagetrennt, CIDR erlaubt), * = jeder Hop |
loopback (compose.yaml setzt auto) |
REQUIRE_HTTPS |
HTTP-Anfragen ablehnen (426) | true |
PUBLIC_URL |
Öffentliche URL (redirect_url für SumUp-Hosted-Checkout) | — |
SUMUP_API_KEY |
SumUp API Key (me.sumup.com → Developer Settings) | — |
SUMUP_MERCHANT_CODE |
SumUp Merchant Code (Pflicht für Cloud API) | — |
SUMUP_CURRENCY |
Währung | EUR |
SUMUP_AFFILIATE_KEY |
Affiliate Key (Cloud API; Toolkit → Affiliate Keys) | — |
SUMUP_APP_ID |
App-ID, z. B. at.bar.vereinsbar |
— |
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS |
Mail-Versand (Alerts, PIN-Reset, Mahnungen) | — / 587 / — / — |
SMTP_FROM |
Absender-Adresse | noreply@barsystem.local |
IBAN / BIC / ACCOUNT_HOLDER |
Konto für EPC-QR-Codes | — |
API_ONLY |
Nur API, kein Frontend | false |
CORS_ORIGIN |
Explizite CORS-Origin (leer = same-origin) | — |
RATE_LIMIT_WINDOW / RATE_LIMIT_MAX |
Rate-Limit-Fenster/Requests | 15 / 200 |
RATE_LIMIT_DUMP |
Rate-Limit-Zustand über Restart speichern (0 = aus) |
1 |
RATE_LIMIT_DUMP_PATH |
Pfad der Rate-Limit-Dump-Datei | data/rate-limit-dump.json |
AUDIT_LOG_DAYS |
Audit-Log-Rotation | 90 |
ADMIN_EMAIL |
Admin-E-Mail für Alerts | — |
PORTAL_RESET_MAX_ATTEMPTS |
Max Fehlversuche PIN-Reset | 5 |
PBKDF2_ITERATIONS |
Passwort-Hash-Iterationen (Tests: 1000) | 600000 |
PASSWORD_MAX_LENGTH |
Max Passwort-/PIN-Länge | 256 |
PASSWORD_MIN_LENGTH |
Min Passwort-/PIN-Länge | 8 |
INITIAL_ADMIN_PASSWORD |
Optional: Bootstrap-Admin-Passwort (sonst Setup-Wizard); kein Default | — |
INITIAL_MANAGER_PASSWORD / INITIAL_STAFF_PASSWORD |
Optionale Bootstrap-Zugänge | — |
BACKUP_ENCRYPTION_KEY |
32-Byte-Schlüssel AES-256-GCM | — (Docker: automatisch in data/secrets.env) |
SETUP_TOKEN |
Setup-Wizard-Token (HTTPS/Prod) | — (Docker: automatisch + Log) |
AUTO_SECRETS |
Fehlende Secrets beim Container-Start erzeugen (1/0) |
1 |
VPS_KNOWN_HOSTS |
known_hosts für strikte SCP-Prüfung | Systemdatei |
APP_UID / APP_GID |
Container-User (Docker) | 1001 |
Docker (optional)
cp .env.example .env
# compose.yaml allein zieht nur aus der Registry (Produktion) —
# gebaut wird lokal/auf dem VPS über den Override:
docker compose -f compose.yaml -f compose.build.yaml up -d --build
compose.yaml bindet standardmäßig an 127.0.0.1; Daten unter ./data/.
Bind-Mount & Dateirechte: Der Entrypoint
chown-t./databeim Start auf den Container-User (APP_UID/APP_GID, Default1001). Läuft der Host-User unter einer anderen UID (z. B.1000), verliert er den Schreibzugriff aufdata/bar.db. Fix:APP_UID/APP_GIDin.envauf die Host-UID setzen (oder ein Named Volume statt Bind-Mount verwenden).Legacy-Backups (unverschlüsselt): Vor der Verschlüsselung erzeugte Backups (
data/backups/barsystem_backup_*.db.gz, ggf.baseline_pre_features_*.db) enthalten Mitglieder-PII im Klartext und sind seit dem Update über die Backup-UI sichtbar/löschbar. Aufräumen:rm data/backups/barsystem_backup_*.db.gz.
API
Alle REST-Endpunkte sind direkt im Source Code definiert — eine Route pro
Datei unter src/routes/ (auth.js, members.js, products.js, checkout.js,
topups.js, terminal.js, portal.js, events.js, shifts.js, reports.js, zReport.js,
settings.js, backups.js, auditLog.js, fees.js, maintenance.js, setup.js …).
Detaillierte Endpoint-Referenz: API.md.
Entwicklung
npm test # Jest parallel (--maxWorkers=50%), Savepoint-Isolation
npm run test:serial # deterministischer serieller Lauf (--runInBand)
npm run test:frontend # jsdom-Frontend-Suiten
npm run lint # eslint src/ server.js tests/ public/js/ (0 Fehler Pflicht)
npm run format # prettier über Backend + public/js + scripts (printWidth 100, singleQuote)
node scripts/check-migrations.js # Migrations-Dry-Run (readonly)
- Neue Routen: in
server.jsUND im Integrationstest-App-Setup registrieren. - Neue Service-Funktionen: direkt aus dem Heimatmodul importieren (kein Barrel mehr, B3).
- Migrationen: nur neue Datei in
src/db/migrations/anlegen (v45+), Signatur{ version, name, up(db) }. - Frontend: KEINE Inline-
onclick=/onchange=-Handler mehr —data-actiondata-args='${JSON.stringify([...])}'verwenden (siehe AGENTS.md).
- Test-Anpassung: Tests, die buggy Verhalten asserten, aktualisieren statt
löschen;
global.fetch-Mocks inafterEachwiederherstellen.
Bekannte offene Punkte
Siehe KNOWN_ISSUES.md — lebende Liste der offenen Punkte (Operator-Aufgabe
R-20, SumUp-Sandbox + Solo-Hardware, L12-UX-Rest, Idempotenz-/Härtungs-Restpunkte,
Struktur-Rest). Abgeschlossenes steht dort nicht mehr: Historie in CHANGELOG.md
und git log, Betrieb in docs/INFRASTRUCTURE.md.