# ProxLedger – Architektur- und Umsetzungsplan Stand: 2026-09-17 · Version 0.1.0-dev ## 1. Überblick ``` Kundennetz A (NAT) Kundennetz B Betreiber ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────────────┐ │ PVE-Host │ │ PBS-Host │ │ ProxLedger (Docker / LXC) │ │ proxledger-agent│─HTTPS─▶│ proxledger-agent│─────▶│ ┌────────┐ ┌───────────┐ │ │ (systemd-Timer) │ Push │ (systemd-Timer) │ Push │ │ web │ │ worker │ │ └──────────────────┘ └──────────────────┘ │ │Next.js │ │ Cron-Jobs │ │ │ └───┬────┘ └─────┬─────┘ │ │ └──── Postgres ┘ │ └──────────────────────────────┘ │ SMTP (M365) / Webhook ▼ E-Mail, Matrix, Zabbix, HA ``` - **Push-Modell:** Das Dashboard fragt nie aktiv bei Hosts an. Agents melden sich täglich per HTTPS. - **web:** Next.js (App Router) liefert UI, Agent-API (`/api/agent/v1/*`) und `install.sh`/`proxledger-agent.sh`. - **worker:** Derselbe Code-Stand, eigener Prozess (`node dist/worker.js`) für zeitgesteuerte Aufgaben: Warnregeln auswerten, „keine Meldung seit 48 h“, Benachrichtigungen versenden, Zusammenfassungen. - **PostgreSQL:** einzige Datenhaltung; PDF-Uploads auf einem Volume (`/data/uploads`), Metadaten in der DB. ## 2. Tech-Stack | Bereich | Wahl | Begründung | |---|---|---| | Framework | Next.js (App Router), TypeScript strict | Vorgabe | | ORM | Prisma + PostgreSQL | Vorgabe, Migrationen versioniert | | UI | `admin-lte` 4.x **nur CSS** + Bootstrap 5.3 + eigene React-Komponenten | AdminLTE 4.9.1 ist stabil, jQuery-frei; die Vanilla-JS-Plugins (PushMenu, Treeview, CardWidget) manipulieren das DOM direkt und werden als React-Komponenten nachgebaut | | Icons | Bootstrap Icons | AdminLTE-4-Standard | | Schrift | Source Sans 3 über `next/font` (selbst gehostet) | kein externer Request zur Laufzeit | | Validierung | zod (API und Server Actions) | Vorgabe | | Auth | Auth.js (OIDC authentik + Credentials-Fallback) | ausgereift, App-Router-kompatibel | | Passwörter/Tokens | argon2id (Passwörter), SHA-256 über 256-Bit-Zufallstoken (API-Tokens) | Token haben volle Entropie, SHA-256 reicht und ist schnell nachschlagbar | | Scheduler | `croner` im Worker | leichtgewichtig, zeitzonenfähig | | E-Mail | nodemailer (SMTP, STARTTLS 587, M365-tauglich) | | | PDF | `@react-pdf/renderer` | kein Chromium im Container | | Tests | Vitest; Integrationstests gegen Postgres (Docker) | | | Agent | Bash, `curl`, Bordmittel; systemd-Service + Timer | Vorgabe | Zeitzone der Anzeige: `Europe/Berlin`, Datenhaltung in UTC. Datumsformat `dd.MM.yyyy`, Währung EUR. ## 3. Datenmodell Gegenüber dem Vorschlag im Auftrag zwei bewusste Änderungen: 1. **Host-Identität ist nicht die Server-ID.** Die Server-ID ändert sich bei Hardwaretausch/Neuinstallation – genau das soll eine Warnung auslösen. Ein Host wird daher über seinen Agent-Token bzw. eine beim Enrollment erzeugte `agentId` (abgeleitet aus `/etc/machine-id`) identifiziert; die Server-ID ist ein Attribut mit Historie. Meldet ein neu registrierter Agent eine Server-ID, die bereits ein bestehender Host hatte, wird das als Neuinstallation erkannt und zur Übernahme vorgeschlagen. 2. **Rechnungen sind eigene Entitäten mit Positionen.** Eine Proxmox-Bestellung enthält oft mehrere Keys, eine Kundenrechnung oft mehrere Hosts. Jede Laufzeit verweist auf höchstens eine Einkaufs- und eine Verkaufsposition. Marge = Verkauf netto − Einkauf netto der Position(en). ### Entitäten (vereinfacht) ``` Customer 1─n Site Customer 1─n Host (customerId nullable → „nicht zugeordnet“) Site 1─n Host Host 1─n Report Host 1─n SubscriptionKey (Key-Historie: key, firstSeen, lastSeen) Host 1─n ServerIdHistory Host 1─n SubscriptionPeriod (Laufzeit: startDate, endDate, key, status: vorgeschlagen|bestätigt) SubscriptionPeriod n─1 PurchaseInvoiceItem ─n─1 PurchaseInvoice (Proxmox) SubscriptionPeriod n─1 SalesInvoiceItem ─n─1 SalesInvoice (Kunde) Host 1─n AgentToken (Hash, erstellt, zuletzt benutzt, widerrufen) EnrollmentToken (Hash, gültig bis, max. Nutzungen, optional customerId/siteId) Alert (type, severity, hostId?, customerId?, periodId?, fingerprint, openedAt, resolvedAt, acknowledgedAt) Notification (alertId?, channel, sentAt, status) – Deduplizierung über Alert-Fingerprint AlertRuleSetting (key, enabled, thresholds JSON) NotificationChannel (type: email|webhook, config JSON, enabled) User (email, name, role: admin|readonly, passwordHash?, oidcSubject?) AuditLog (userId?, action, entity, entityId, diff JSON, createdAt) Tag n─m Host Attachment (PDF: path, mime, size, sha256) – an PurchaseInvoice / SalesInvoice ``` **PurchaseInvoice (Proxmox):** Rechnungsnummer, Rechnungsdatum, Fälligkeitsdatum, netto, brutto, Rabatt, Zahlungsart, bezahlt (bool), Bezahldatum, PDF. **SalesInvoice (Kunde):** erstellt (bool), Rechnungsnummer, Rechnungsdatum, Betrag netto/brutto, vom Kunden bezahlt (bool), Bezahldatum, PDF. USt-Satz standardmäßig 19 %, pro Rechnung überschreibbar. Beträge als `Decimal(12,2)`. Das „Häkchen direkt in der Tabelle“ setzt den Status der verknüpften Rechnung. Existiert noch keine Rechnung, legt das Häkchen eine Minimal-Rechnung an (Nummer später nachtragbar). ## 4. Agent-API Basis: `/api/agent/v1`, nur JSON, strikt getrennt vom UI (eigene Route-Group, keine Session-Cookies, eigenes Rate-Limit). | Methode | Pfad | Auth | Zweck | |---|---|---|---| | POST | `/enroll` | Enrollment-Token (Bearer) | Host registrieren → liefert `hostId` + host-spezifischen API-Token (einmalig im Klartext) | | POST | `/report` | Host-Token (Bearer) | Statusbericht; Antwort enthält `latestAgentVersion`, `serverTime` | | GET | `/ping` | Host-Token | Verbindungstest (`proxledger-agent --test`) | | GET | `/install.sh`, `/agent.sh` | öffentlich | Installer und Agent-Skript (Version eingebettet) | - zod-Schemas mit `nullable()` für alle optionalen Felder; unbekannte Felder werden ignoriert und geloggt. - Rate-Limit: Token-Bucket im Prozess (pro IP und pro Token), z. B. 10 Anfragen/Minute; `429` mit `Retry-After`. - Report-Verarbeitung in einer Transaktion: Report speichern → Host-Stammdaten aktualisieren → Key-/Server-ID-Historie → Laufzeitvorschlag bei neuem `nextduedate` → betroffene Warnregeln neu auswerten. - Nachgesendete (gepufferte) Reports tragen ihren Original-Zeitstempel (`collectedAt`) und werden idempotent über `reportId` (UUID vom Agent) angenommen. ## 5. Agent - `install.sh`: prüft root/Produkt, lädt `proxledger-agent.sh` vom Dashboard, `enroll`, schreibt `/etc/proxledger/agent.conf` (600), installiert `proxledger.service` + `proxledger.timer` (`OnCalendar=daily`, `RandomizedDelaySec=4h`, `Persistent=true`), erster Report sofort. - Optionen: `--token`, `--url`, `--ca-cert`, `--uninstall`, `--update`, `--test`. - Sammelt: Subscription (PVE/PBS/PMG), Hostname, FQDN, IPs, Cluster/Node, Produktversion, Kernel, Sockets, CPU-Modell, Uptime, Anzahl Updates, Agent-Version, Zeitstempel. Fehlende Werte → `null`. JSON wird ohne `jq` erzeugt (eigene Escape-Funktion). - Fehlschlag: 3 Versuche mit Backoff (30 s, 2 min, 8 min), danach Ablage in `/var/lib/proxledger/spool/`, beim nächsten Lauf nachsenden (max. 30 Dateien). - Logging über `logger -t proxledger` bzw. stdout ins Journal. - Update: nur wenn `AUTO_UPDATE=yes` in der Config oder manuell via `--update`; Prüfsumme vom Dashboard wird verifiziert. ## 6. Warnregeln Regeln sind reine Funktionen `(Kontext) → AlertCandidate[]`, getrennt testbar. Der Worker gleicht Kandidaten mit offenen Alerts ab (Fingerprint = Regel + Objekt-ID [+ Schwelle]): neue öffnen, verschwundene schließen, bestehende unverändert lassen. Benachrichtigt wird nur beim Öffnen bzw. beim Wechsel auf eine höhere Stufe (30 → 14 → 7 Tage). | Regel | Standard | |---|---| | Subscription läuft ab | 30 / 14 / 7 Tage | | Status ≠ active | sofort | | Sockets lizenziert ≠ physisch | sofort | | Keine Meldung | 48 h | | Server-ID geändert | sofort | | Key von mehreren Hosts gemeldet | sofort | | Host ohne Kunde | sofort (nur Info) | | Proxmox-Rechnung unbezahlt, Fälligkeit | ≤ 7 Tage | | Verlängert/bezahlt, Kundenrechnung fehlt | sofort | | Kundenrechnung unbezahlt | nach 30 Tagen | ## 7. Sicherheit - UI-Routen per Middleware geschützt (ab #013). **Bis dahin nicht öffentlich betreiben.** - Tokens: 32 Byte Zufall, Präfix `plx_e_` (Enrollment) / `plx_h_` (Host), in der DB nur SHA-256-Hash; einzeln widerrufbar. - Audit-Log für alle schreibenden Server Actions (vor #013 mit Akteur „system“). - Security-Header (CSP, HSTS hinter Reverse Proxy), Upload-Prüfung (nur PDF, max. 10 MB). ## 8. Projektstruktur ``` proxledger/ ├─ agent/ Bash-Agent, Installer, systemd-Units ├─ deploy/ docker-compose, LXC-Installer, Beispiel-Proxy-Configs ├─ docs/ Architektur, Issues ├─ prisma/ schema.prisma, migrations, seed.ts ├─ public/ favicon.svg, Icons ├─ src/ │ ├─ app/ │ │ ├─ (dashboard)/ UI-Seiten (AdminLTE-Layout) │ │ ├─ (auth)/login/ │ │ └─ api/agent/v1/ Agent-API │ ├─ components/ Layout, Logo, SmallBox, Card, Badge, DataTable … │ ├─ lib/ db, auth, tokens, validation, format │ ├─ server/ Services (reports, alerts, invoices, notifications) │ ├─ worker/ Scheduler-Einstiegspunkt │ └─ mock/ Beispieldaten für den Prototyp └─ tests/ ``` ## 9. Meilensteine | Version | Inhalt | Issues | |---|---|---| | 0.1.0 | Projekt-Setup, Prototyp, Datenmodell, Agent-API | #001–#004 | | 0.2.0 | Agent-Skript | #005 | | 0.3.0 | Kunden, Standorte, Hostzuweisung, Enrollment-Tokens | #006 | | 0.4.0 | Host-Tabelle, Suche, Detailansichten | #007 | | 0.5.0 | Laufzeiten und Rechnungen | #008 | | 0.6.0 | Kundenübersicht, Dashboard live, Jahresbahn, Export | #009, #010 | | 0.7.0 | Warnregeln | #011 | | 0.8.0 | Benachrichtigungen | #012 | | 0.9.0 | Authentifizierung, Audit-Log | #013 | | 0.10.0 | Deployment (Docker, LXC), Doku | #014 | | 1.0.0 | Feinschliff, Dark-Mode, Mobil, Härtung | #015 |