docs: Projektorganisation, Architekturplan und Issues pro Phase (#001)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 19:42:04 +02:00
co-authored by Claude Opus 5
commit a54c1b6285
20 changed files with 721 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
# 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 |