Accountant: Inventar, where, Checkmk, Abrechnung

This commit is contained in:
br committed 2026-10-18 17:08:58 +02:00
1 parent 9f16148cde
commit dd93277750
59 files changed
+7522 -113

No files matched your search

+100 -1
View File
@@ -493,6 +493,7 @@ Der Agent muss deshalb nach jedem (Wieder-)Verbinden seinen Zustand erneut sende
| `--node-id` | aus dem Hostnamen | z. B. `Proxmox-VM.42` → `proxmox-vm-42` |
| `--docker-socket` | `/var/run/docker.sock` | `none` = ohne Docker |
| `--backend-host` / `--backend-interface` | automatisch | siehe Backend-Adresse |
| `--fqdn` / `SULTAN_FQDN` | automatisch | Name des Hosts fürs Inventar (5.3), nur Information |
* **Enrollment:**
1. Der Agent erzeugt `node.key` und prüft den Server gegen den Fingerprint aus der Einladung.
@@ -614,6 +615,87 @@ Der Agent muss deshalb nach jedem (Wieder-)Verbinden seinen Zustand erneut sende
### 5.3 Accountant: zentrales Inventar & Abrechnung (Entscheidung 2026-10-16; 6a–6c umgesetzt)
**Zweck (Priorität):** (1) Im Störfall sofort wissen, auf welchem Backend-Host und in welchem Compose-Stack eine URL läuft – ohne Umweg über DNS, Reverse-Proxy und VPN-Suche. (2) Inventar je Mandant (Grundlage für spätere Statusberichte an Mandanten). (3) Abrechnung markierter Services.
**Rollen:**
* **Accountant** – `sultan accountant` (gleiches Binary, eigene Rolle): nimmt Berichte entgegen, führt Inventar und Historie. Eigenes Datenverzeichnis mit **eigener `accountant.db`**, eigenem TLS-Zertifikat (gepinnt wie beim Server) und eigenem Agent-Port (Default `:8444`).
* **Payee** – ein normaler `sultan server` (Ingress mit Traefik), der zusätzlich an den Accountant meldet: `--accountant=https://…:8444` (bzw. `SULTAN_ACCOUNTANT`) plus Einladung des Accountants. Nur Server melden; Agents (Docker wie systemd) sind nicht meldepflichtig, sie liefern ihrem Server lediglich Zusatzdaten.
**Warum eigene DB:** andere Lebensdauer (Historie über Jahre, eigene Aufbewahrung/Backups), anderes Vertrauen (Berichte sind Eingaben fremder Server und werden nie mit dem eigenen Mandanten- und Schlüsselbestand vermischt), kein Ballast und keine Migrationen auf jedem Ingress. Migrationsmechanik und Zeitformat werden aus `pkg/store` wiederverwendet.
**Transport & Sicherheit:** wie Agent → Server. Payee erzeugt lokal `payee.key` (Ed25519, im Datenverzeichnis des Servers), löst eine Einladung `sltn_enroll_<secret>.<fingerprint>.<url>` des Accountants ein (`sultan accountant invite --payee=<id>`), pinnt dessen Fingerprint, signiert jeden Request (`pkg/auth`). Nur TLS 1.3, kein Klartext.
**Bericht = vollständiger Schnappschuss** (`POST /api/v1/report`), kein Delta: sofort nach Änderungen (Knoten online/offline, `service_state`, Mandantenänderung; 10 s gebündelt) und stündlich als Lebenszeichen. Inhalt:
* Payee: Name, Version, Zeitpunkt.
* Mandanten: ID, Name, Tier, Status.
* Knoten: ID, Mandant, Hostname, **FQDN**, IP, Status, `last_seen`, Betriebssystem, Docker-Version, NetBird-IP (falls vorhanden).
* Routen: Router, Hosts, Mandant, Knoten, Ziel als **`<fqdn>:<port>`** und `<ip>:<port>`, Container, **Compose-Projekt und -Verzeichnis**, Startzeit, Docker-Health, Zustand (läuft / down / Wartung / Mahnstufe), Produkt aus `sultan.accountant.billing`.
**Zusatzdaten vom Agent** (umgesetzt 2026-10-16; nur Information, ändern das Routing nie; **ohne zusätzliche Labels**, alles automatisch erkannt):
* je Container `meta` im Workload: `compose_project`, `compose_service`, `compose_dir` (aus den Labels, die Docker Compose selbst setzt: `com.docker.compose.project`, `….service`, `….project.working_dir`), `state`, `started_at`, `health` (aus `GET /containers/{id}/json`, `docker.Client.Inspect`). Ein Health-Wechsel löst über das Docker-Event `health_status` eine neue Meldung aus. sultan.d-Dienste haben kein `meta`.
* je Knoten `host_info` im `service_state`: `fqdn`, `os`, `docker_version`, `netbird_ip`.
* **FQDN:** `--fqdn` > Hostname mit Punkt > Hostname über die Suchdomain aufgelöst (CNAME/kanonischer Name) > Reverse-Lookup der Backend-Adresse > kurzer Hostname; einmal beim Start, je Abfrage max. 2 s.
* **Betriebssystem/Docker-Version:** aus `GET /info` (das OS des Hosts, auch wenn der Agent im Container läuft); ohne Docker `PRETTY_NAME` aus `/etc/os-release`.
* **NetBird-IP:** IPv4 auf `wt0`, sonst die erste Adresse aus 100.64.0.0/10.
* Der Server liest `service_state` tolerant: ältere Server ignorieren die Felder, ältere Agents schicken sie nicht. Die Registry speichert sie mit dem Zustand (`Registry.State`, `Server.NodeState`) für den Bericht an den Accountant.
* Das Ziel `<fqdn>:<port>` je URL entsteht beim Bericht aus `host_info.fqdn` und dem (bereits auf den Host-Port umgerechneten) Service-Port.
**Umsetzung Accountant, Teil 1 (6b, 2026-10-16):**
* `pkg/accountant`: `New` (Datenverzeichnis 0700, Default `./accdata`; `accountant.db`; eigenes `server.crt`/`server.key`), `Start`/`Run`/`Shutdown`, TLS-Port Default `:8444`, `POST /api/v1/enroll` (signiert mit `payee.key`, `auth.Verifier.VerifyUnregistered`; Body `{token, hostname}`, Antwort `{payee_id}`; 403 ohne Details bei unbekannter/verbrauchter/abgelaufener Einladung, 409 bei doppeltem Schlüssel). `--public-url` wird gemerkt (`settings.public_urls`) und steckt in Einladungen.
* `pkg/ledger` (`accountant.db`, Migrationen über `store.OpenDB`): `payees` (ID aus der Einladung, Hostname, Schlüssel, IP, Zeitpunkt), `invites` (nur Hash, einmalig, Ablauf), `settings`. Ein registrierter Payee bekommt keine neue Einladung, bevor er gelöscht ist.
* CLI: `sultan accountant [--addr --public-url --data-dir]`, `accountant invite --payee ID [--ttl] [--server-url]`, `accountant payee list`, `accountant payee delete --id ID`. Verwaltungsbefehle legen nie ein neues Datenverzeichnis an (Schutz vor Tippfehlern).
* Rate-Limit für `/enroll` wie beim Server (`--enroll-limit`, Default 10/min je IP, `pkg/ratelimit`; schützt vor Last und Log-Flut, nicht vor Raten – dafür sorgen 256 Bit Zufall). Compose-Vorlage: `deploy/compose/accountant` (beides umgesetzt 2026-10-18).
**Umsetzung Payee & Berichte (6c, 2026-10-17):**
* Server: `--accountant-token` (`SULTAN_ACCOUNTANT_TOKEN`, Einladung, nur beim ersten Start) und `--accountant` (`SULTAN_ACCOUNTANT`, Adress-Override; Vorrang: Override > Adresse in der Einladung > `accountant.json`). Im Datenverzeichnis des Servers: `payee.key` (0600) und `accountant.json` (`url`, `fingerprint`, `payee_id`). Ohne Einladung und ohne `accountant.json` meldet der Server nichts.
* **Fehlerverhalten:** Unbrauchbare Angaben (Einladung unlesbar, `http://`, Einladung ohne jede Adresse, `--accountant` ohne Einladung) verhindern den Start – das sind Tippfehler. Ein nicht erreichbarer Accountant hält den Ingress **nie** auf: Enrollment und Berichte laufen im Hintergrund und werden je Prüfintervall wiederholt; Fehler werden nur bei Änderung geloggt. Einladung eines **anderen** Accountants bei bestehender Registrierung: Fehler im Log, keine Berichte, Server läuft (kein stiller Wechsel).
* **Wann gemeldet wird:** Der Server baut den Schnappschuss je Prüfintervall (Default 10 s) und sendet nur, wenn sich der Inhalt (Hash ohne Zeitstempel) geändert hat oder das Lebenszeichen fällig ist (Default 1 h). Polling statt Ereignis-Hooks: erfasst auch Mandanten-, Wartungs- und Statusänderungen ohne zusätzliche Kopplung. Fehlgeschlagene Berichte werden beim nächsten Intervall wiederholt.
* **Inhalt** (`accountant.Report`): Mandanten, alle Knoten aus `sultan.db` (verbunden, Wartung, `last_seen`, Host-Infos aus dem letzten `service_state`), Routen aus den gemeldeten Zuständen (`reportRoutes`: Hosts per `HostsFromRule`/`HostsFromSNIRule`, `backend`, `backend_fqdn` = `<fqdn>:<port>`, Compose-Angaben, Produkt aus `sultan.accountant.billing`, `down` aus `down_services`, `active` = Router steht in der Traefik-Konfiguration, also nicht von der Policy verworfen).
* Accountant: `POST /api/v1/report` (signiert, Schlüssel muss registriert sein, max. 8 MiB, `generated_at` Pflicht), speichert den **letzten** Bericht je Payee normalisiert (`reports`, Migration v2 von `accountant.db`). CLI: `payee list` mit `LAST_REPORT`, `payee report --id` (JSON).
**Umsetzung Checkmk (6e, 2026-10-18):**
* `--metrics-addr` (Default `127.0.0.1:8082`, Klartext, nur Loopback; `none` = aus), `GET /metrics/checkmk`. Quelle: letzter Bericht je Payee (keine Historie nötig).
* Services: `Sultan Payee <id>` (OK; WARN ab `--stale-warn` 2 h bzw. ohne Bericht; CRIT ab `--stale-crit` 6 h) und `Sultan URL <host>` je Host jeder Route (OK; **WARN** bei „von Policy verworfen“, „gestoppt/Wartungsseite“, „unhealthy“, „Knoten getrennt“; angekündigte **Wartung** des Knotens bleibt OK mit Hinweis; veralteter Bericht des Payees überträgt WARN/CRIT). Derselbe Host mehrfach → Zusatz `(payee/knoten/router)`.
* Text per `text/template`: eingebaut (`defaultCheckmkTemplate`), eigene `<data-dir>/checkmk/checkmk.tmpl` hat Vorrang und wirkt sofort; fehlerhafte Vorlage → eingebaute greift + CRIT-Service „Sultan Accountant Vorlage“. `sultan accountant dump-checkmk [--force]`. Felder bereinigt (ASCII, einzeilig, `|`→`/`, `"`→`'`) und HTML-escaped; `.Unescaped.<Feld>` nur bereinigt. Funktion `status` (0/1/2 → OK/WARN/CRIT).
* Links: Die Standardvorlage schreibt `https://<host>` und `http://<fqdn>:<port>` als Text; ob Checkmk sie klickbar darstellt, hängt von Version/Einstellung ab – Anpassung über die Vorlage.
**Historie:** Der Accountant führt je (Payee, Mandant, Host, Router) Zeiträume `first_seen` / `ended_at`. Fehlt eine Route in einem *frischen* Schnappschuss, endet ihr Zeitraum (Offboarding). Bleibt das Lebenszeichen eines Payees aus (> 2 h), endet **nichts** – der Server ist weg, nicht seine Services; Checkmk meldet den Payee als veraltet.
**Abrechnung – Modell (Antworten 2026-10-18; Umsetzung 6d).** Der Accountant gewinnt: Das Label `sultan.accountant.billing=<produkt>` ist der bequeme Weg für den Hoster (viele Kunden pflegen ihre Services nicht selbst; der Hoster trägt das Label am Kunden-Container ein), Vorgaben am Accountant haben aber immer Vorrang. **Alle** Routen werden erfasst; nichts verschwindet aus dem Bericht.
* **Kunde** (zentral am Accountant): trägt Zahlweise, Pauschale und **Kostenverursacher-Gruppe** (z. B. `extern`, `intern`, `familie` – zum Clustern, keine Buchhaltungs-Kostenstelle; Posten haben keine eigene) sowie eine **Notiz** für persönliche Nebenabreden; Gruppe und Notiz sind im Bericht sichtbar und filterbar. Mandanten werden Kunden zugeordnet – gleiche Mandanten-ID auf allen Payees = derselbe Kunde (automatisch: der Kunde entsteht beim ersten Auftauchen der Mandanten-ID mit dem Mandantennamen und Zahlweise „nicht eingeordnet“; maßgeblich ist der Mandant aus der Einladung auf dem Server, nicht eine Angabe des Agents – von Hand nur Zahlweise, Pauschale, Gruppe, Notiz und Ausnahmen); eine Zuordnung je Payee + Mandant überschreibt das (Tippfehler auf einem Nebencluster, historische Abweichungen). Mandanten ohne Kunde erscheinen im Bericht als „Kunde unbekannt“.
* **Pauschale** ist ein Posten am Kunden (monatlich); die meisten Kunden zahlen pauschal.
* **Posten = URL bzw. Router:** je Router ein Posten (seine Hosts werden aufgeführt); alle Posten gehören dem Kunden des Mandanten. Einem Posten kann ein „Dienst“ zugeordnet sein (z. B. OLAT) – das ist dann der abgerechnete Zusatz-Service.
* **Klassen eines Postens:**
* `included` – in der Pauschale enthalten (eingepreist), nur aufgeführt;
* `billable` – dazugebuchter Zusatz-Service, eigene Position;
* `free` – Give-away, kostet derzeit nichts, wird erfasst und kann später (mit Gültig-ab-Datum) abrechenbar werden;
* `internal` – keine Rechnung, aber über die Gruppe des Kunden zuordenbar (z. B. Infrastruktur wie `sultan.chmln.de`, Familie) und mit **Schattenpreis**, damit Fragen wie „Was habe ich dem Neffen geschenkt?“ oder „Was müsste das Brautpaar eigentlich zahlen?“ beantwortbar sind; ein Posten kann später an einen echten Kunden übergehen.
* **Klasse/Produkt einer Route** (erste zutreffende Regel): Zuordnung am Accountant für Route/Domain > Label `sultan.accountant.billing` (Produkt aus dem Katalog bestimmt die Klasse) > Kunde mit Pauschale: `included` > sonst „nicht zugeordnet“ (fällt im Bericht auf). Klassen- und Preisänderungen gelten ab Datum.
* **Laufzeit:** Abrechnung immer in vollen Monaten (Stichtag Monatserster/Monatsultimo). **Beginn:** Der Monat, in dem ein Posten erstmals auftaucht, zählt voll ab dem 1. Liegen Go-live und Bestellung weit auseinander, setzt der Hoster den Beginn je Posten manuell auf einen späteren Monatsersten (die Zwischenzeit läuft als Einmalbetrag im Projekt, außerhalb von Sultan). Gestoppte Routen, Wartung, Mahnstufen ändern nichts – erst das Verschwinden aus dem Bericht beendet einen Posten. SLA-Verletzungen werden manuell vergütet (bleibt so). Kündigung: Standard zum Monatsultimo; zusätzlich gibt der Bericht eine **tagesgenaue Gegenrechnung** als Vorschlag aus (beschleunigt seltene Kündigungen).
* **Zahlweise je Kunde:** monatlich, quartalsweise, jährlich – oder **„noch nicht eingeordnet“** (Feedback der Buchhaltung fehlt). Die Monats-CSV weist bei Quartals-/Jahreszahlern den Monatsanteil aus und vermerkt, dass die Rechnung das 3- bzw. 12-Fache beträgt (bzw. führt den Periodenbetrag mit); bei „nicht eingeordnet“ steht ein Vermerk.
* **Preise** im Accountant: **optional** (Katalogpreis je Produkt, Kundenpreis überschreibt, Gültig-ab). Nicht buchhaltungsrelevant – führend sind die Abos in Lexware Office; Preise dienen Summen, Plausibilität und Schattenpreisen. Ohne Preis bleibt die Spalte leer.
* **Ausgabe:** CSV je Monat (Abrechnung erfolgt in Lexware Office). Spalten werden an der Praxis nachgeschärft; kein Templating, bis die Buchhaltungsfragen geklärt sind.
* **Geparkt:** Abo-Revision (Abgleich mit den Lexware-Abos) bleibt vorerst manuell. **Ausblick (TODO):** Der Accountant als „Realität“ statt der Abopflege in Lexware – also Lexware aus dem Bestand speisen statt umgekehrt.
* **Offene Fachfragen vor 6d:** siehe Abschnitt 9.
**Abfrage:**
* CLI auf dem Accountant: `sultan accountant where <domain>` (Mandant, Payee, Knoten, `<fqdn>:<port>`, Container, Compose-Verzeichnis, Zustand), `routes [--tenant=…]`, `billing --month=JJJJ-MM`, `payee list`, `invite`.
* Checkmk (`GET /metrics/checkmk` auf Loopback, umgesetzt 2026-10-18, siehe unten): ein Service je URL mit Link auf `https://<host>` und auf das interne Ziel `http://<fqdn>:<port>`, dazu Frische je Payee. **Text per Vorlage** (Go `text/template`, wie beim Lander): eingebaute Default-Vorlage, eigene unter `<data-dir>/checkmk/` hat Vorrang; `sultan accountant dump-checkmk` exportiert die Vorlage zum Anpassen. Die Default-Vorlage wird mit den Releases weiterentwickelt. **Werte aus Berichten sind Fremdeingaben** (Containername, Compose-Verzeichnis stammen vom Kunden-Host): Sie werden vor dem Einsetzen bereinigt (keine Zeilenumbrüche, kein `|`, ASCII) und HTML-escaped; rohe Werte nur über eine explizit benannte Funktion. Grund: Wer in Checkmk das HTML-Escaping abschaltet, damit Links klickbar sind, öffnet sonst Script-Injektion in die Checkmk-Oberfläche.
* Später: HTTP-API als Vorstufe für den Export ins Dokuwiki; Statusberichte an Mandanten. (Uptime Kuma verworfen: keine offizielle API.)
**Umsetzung Historie, `where`, `routes` (6d Schritt 1, 2026-10-18):**
* `accountant.db` v3: `route_spans` – ein Zeitraum je **Ort** (Payee, Mandant, Knoten, Protokoll, Router) mit Hosts, Backend, `<fqdn>:<port>`, Workload, Compose-Verzeichnis, Produkt, `first_seen`/`last_seen`/`ended_at`. `SaveReport` führt sie im selben Schritt wie den letzten Bericht fort: neue Orte öffnen, bestehende übernehmen geänderte Angaben (`first_seen` bleibt), fehlende enden mit dem Eingang des Berichts. Nur ein Bericht des Payees selbst beendet dessen Orte; ein schweigender Payee beendet nichts. Die Historie bleibt, wenn ein Payee gelöscht wird. Ein Umzug auf einen anderen Knoten ist ein neuer Ort; die Abrechnung fasst Orte später je Mandant + Router zum Posten zusammen.
* `sultan accountant where <host|mandant>`: Teiltreffer im Host (ohne Groß-/Kleinschreibung) oder exakte Mandanten-ID; je Treffer URL, Mandant, Ingress (Zeitpunkt des Berichts), Backend `<fqdn>:<port>` (+ IP, NetBird), Container + Compose-Verzeichnis, Zustand, Knoten (verbunden, OS, Docker), **seit** wann am Ort, **vorher** (beendete Orte derselben Route), dazu passende Routen, die es **nicht mehr** gibt. Kein Treffer → Exit 1.
* `sultan accountant routes [--tenant] [--payee]`: Tabelle aller aktuellen Routen (Host, Mandant, Payee, Knoten, Ziel, Zustand, seit).
**Umsetzung Abrechnung (6d Schritte 2–4, 2026-10-18):**
* **Kunden** (`accountant.db` v4: `customers`, `customer_map`): `StoreReport` (Endpunkt) ruft `EnsureCustomers` – unzugeordnete Mandanten-IDs werden Kunden (`auto`, Name folgt dem Mandanten, bis der Kunde von Hand bearbeitet wird). Auflösung `CustomerResolver.Resolve(payee, mandant)`: Zuordnung für den Payee > für alle Payees > Kunde mit gleicher ID. CLI `customer add|set|list|map|unmap|delete`; `set` ändert nur angegebene Felder; `list --group`, `--unclassified`. `where` zeigt den Kunden.
* **Katalog** (v5: `products`, `product_versions`, `customer_prices`, `assignments`): Klasse und optionaler Preis je Monat (`product set --from JJJJ-MM`, erste Version gilt von Anfang an), Kundenpreis mit Vorrang. **Zuordnung je Posten** (Mandant, Protokoll, Router) mit Produkt, Klasse und/oder Beginn: `assign --host …` (Host → Posten über die Historie; mehrdeutig → `--tenant/--router`), `assign --clear`, `assignments`.
* **Monatsabrechnung** (`Billing`, `billing --month JJJJ-MM [--out]`): Posten = alle Orte eines (Mandant, Protokoll, Router); abgerechnet, wenn der Monat zwischen Beginn (Zuordnung, sonst Monat des ersten Auftauchens, voll) und dem Monat des Endes (bis Ultimo) liegt. Klasse: Zuordnung > Produkt (Zuordnung, sonst Label) > Pauschale des Kunden → `included` > leer („nicht zugeordnet“; Produkt nicht im Katalog wird benannt). Beträge: billable = Preis (fehlt → Hinweis), included 0, free/internal 0 + Schattenpreis. Pauschale als eigene Zeile je Kunde mit Pauschale (Hinweis, wenn keine URL aktiv). Periodenbetrag = Monatsbetrag × 1/3/12; Hinweise für Quartals-/Jahreszahler und „noch nicht eingeordnet“; Ende im Monat → „beendet am …“ + tagesgenaue Gegenrechnung. Mandanten ohne Kunde: Hinweis „Kunde unbekannt“, am Ende der Liste.
* **CSV:** UTF-8 mit BOM, Semikolon, Dezimalkomma; Spalten Monat, Kunde, Kundenname, Gruppe, Zahlweise, Posten, Hosts, Mandant, Klasse, Produkt, Von, Bis, Monatsbetrag, Periodenbetrag, Schattenpreis, Hinweis, Notiz; je Kunde die Pauschale zuerst. Spalten werden an der Praxis nachgeschärft.
---
## 6. Überwachung, Lizenzfristen & Alerting (Checkmk-Integration)
@@ -672,7 +754,7 @@ Das Ausgabeformat entspricht der nativen Checkmk Local-Check-Syntax:
## 7. Referenzkonfigurationen
Die lauffähigen Vorlagen liegen unter `deploy/compose/{server,agent}` (mit `.env.example`); die Ausschnitte hier zeigen das Prinzip. Image: ein distroless-Image für Server und Agent (`Dockerfile`, nonroot-User `65532`, kein `ENTRYPOINT`, Arbeitsverzeichnis `/`, Default-Datenverzeichnis also `/suldata`). Ein Bind-Mount für `/suldata` muss vorher `65532:65532` gehören.
Die lauffähigen Vorlagen liegen unter `deploy/compose/{server,agent,accountant}`; die Ausschnitte hier zeigen das Prinzip. **Einstellungen nur über `.env`** (`env_file: .env`): `compose.yaml` wiederholt keine Variablen, alle Optionen samt Defaults stehen in `.env.example` (`SULTAN_<FLAG>` je Flag, leer = Default). Das Image legt `/suldata` und `/accdata` für den nonroot-User an; `WORKDIR /` macht beide zu den Defaults. Image: ein distroless-Image für Server und Agent (`Dockerfile`, nonroot-User `65532`, kein `ENTRYPOINT`, Arbeitsverzeichnis `/`, Default-Datenverzeichnis also `/suldata`). Ein Bind-Mount für `/suldata` muss vorher `65532:65532` gehören.
### 7.1 Compose-Setup: Ingress-Knoten
@@ -803,6 +885,15 @@ services:
| 2026-10-02 | **Kleinere Punkte:** Rate-Limit `/enroll` 10/min/IP vor der Signaturprüfung (`-1` = aus); `SULTAN_<FLAG>` für alle Flags, Flags gewinnen; „wieder online“ nur nach Offline-Alarm; Checkmk-Zeiten in Server-Zeitzone. Ein Agent, dessen Enrollment mit 429 scheitert, beendet sich; der Neustart erfolgt über systemd bzw. Docker `restart`. | Schutz vor Durchprobieren; Container-/systemd-freundliche Konfiguration; vollständige Alarmkette. |
| 2026-10-03 | **Gestoppter Container = Wartungsseite, entfernter = Route weg.** Der Agent meldet gestoppte Container mit (Ports aus `PortBindings`) und deren Services als `down_services`; laufende Container gewinnen bei Namensgleichheit. Leerer `--public-url`-Wert wird ignoriert; `sultan version` zeigt die Zeitzone; `TZ` kommt über Compose (distroless enthält tzdata). Server-Compose mit `network_mode: host` neben Traefik. | Erster Test gegen echtes Traefik: `docker stop` lieferte 404 statt 503. Abschalten bleibt über Entfernen möglich. |
| 2026-10-11 | **TCP nur als TLS-Passthrough** (Feature `tcp_stream`, auch Community): `HostSNI` mit expliziten Hosts aus `allowed_domains`, keine Wildcard-/Regexp-Router; nur Entrypoint `websecure`; zählt gegen `max_routes`. Ohne Interceptor: Mahnhinweis lässt durch, ab Paywall und bei Wartung Route entfernt. UDP und übrige TCP-Labels weiterhin abgelehnt. | Kunden mit eigener CA/TLS (tinyCA) brauchen Passthrough; DNS-Wildcard ist kein Grund für einen Wildcard-Router – host-genau bleibt prüf- und abrechenbar. Seiten lassen sich bei TCP nicht einblenden. |
| 2026-10-16 | **Accountant** (5.3): zentrales Inventar über eigene Rolle `sultan accountant` mit eigener `accountant.db`; nur Server (Payees, `--accountant`) melden, per signiertem, gepinntem Push eines vollständigen Schnappschusses bei Änderung und stündlich. Abrechnung: Vorgaben am Accountant (Zuordnung, Abo pauschal/einzeln, Mandantenpreis) gewinnen vor dem Label `sultan.accountant.billing`; alle Routen werden erfasst, ungelabelte gelten bei Abo als „im Abo enthalten“, sonst „nicht zugeordnet“. Abfrage zuerst per CLI (`where`) und Checkmk (Text per `text/template`-Vorlage mit eingebautem Default); HTTP-API später, Uptime Kuma verworfen. | Störfall: Backend-Host sofort finden; Kunden-Labels können Routen nicht vor der Abrechnung verstecken; bewährte Mechanik (Pinning, Signatur, Schnappschuss) statt neuem Protokoll; viele Services sind eingepreist; Vorlagen-Mechanik wie beim Lander, ohne neue Abhängigkeit. |
| 2026-10-16 | **6a/6b umgesetzt:** Zusatzdaten automatisch aus Docker (Compose-Labels, Inspect, `/info`), DNS und Interfaces – keine neuen Pflicht-Labels; einziges neues Label ist `sultan.accountant.billing` (statt `sultan.billing`). Accountant mit Default `./accdata` und Port `:8444`; `accountant.db` über das exportierte `store.OpenDB`; Payee-ID kommt aus der Einladung (`invite --payee`). | Kein Pflegeaufwand an Kunden-Compose-Dateien; Label-Namensraum zeigt, wer das Label auswertet; getrenntes Datenverzeichnis erlaubt Accountant und Server auf demselben Host. |
| 2026-10-17 | **6c umgesetzt:** Payee meldet per Polling (Prüfintervall 10 s, Senden nur bei geändertem Inhalt, Lebenszeichen 1 h) statt über Ereignis-Hooks; Konfigurationsfehler verhindern den Start, ein nicht erreichbarer Accountant nie; Einladung eines anderen Accountants wird bei bestehender Registrierung ignoriert (Log-Fehler). Accountant hält bis 6d nur den letzten Bericht je Payee. Routen tragen `active` (in Traefik) und `down`. | Ingress-Verfügbarkeit vor Inventar; Polling erfasst alle Änderungsquellen ohne Kopplung; kein stiller Wechsel der Zentrale. |
| 2026-10-18 | **Compose: Einstellungen nur über `.env`** (`env_file`), Defaults dokumentiert in `.env.example`; Accountant-Vorlage `deploy/compose/accountant` mit Datenverzeichnis `/accdata`. | Keine doppelte Pflege von Variablen und Defaults; neue Flags brauchen keine Compose-Änderung. |
| 2026-10-18 | **Abrechnungsmodell** (5.3): Posten = Router/URL; Pauschale am Kunden; Klassen `included`/`billable`/`free`/`internal` (Kostenstelle, Schattenpreis); volle Monate, Kündigung zum Ultimo mit tagesgenauer Gegenrechnung als Vorschlag; Zahlweise m/q/j je Kunde mit Vermerk in der Monats-CSV; Kunden-Zuordnung automatisch über gleiche Mandanten-ID, Ausnahme je Payee. | Spiegelt die Praxis (Lexware-Abos, meist Pauschale, Zusatz-Services je URL); interne und geschenkte Dienste bleiben bewertbar. |
| 2026-10-18 | **Abrechnung, Details:** Beginn = voller Monat ab dem 1. des ersten Auftauchens, manuell verschiebbar je Posten; Zahlweise je Kunde; Preise optional und nicht buchhaltungsrelevant (Lexware führt); Gruppe (Kostenverursacher) und Notiz (Nebenabreden) je Kunde, filterbar; Abo-Abgleich bleibt manuell. | Projekt- und Einmalbeträge laufen außerhalb; persönliche Kundenverhältnisse bleiben nachvollziehbar, ohne Buchhaltungsstruktur nachzubauen. |
| 2026-10-18 | **6e umgesetzt:** Checkmk des Accountants auf eigenem Loopback-Port `127.0.0.1:8082`, Quelle = letzter Bericht; Wartung dämpft auf OK; Rate-Limiter nach `pkg/ratelimit` verschoben und für `/enroll` des Accountants genutzt. | Unabhängig von 6d nutzbar; einheitliches Verhalten von Server und Accountant. |
| 2026-10-18 | **Historie je Ort** (Payee, Mandant, Knoten, Protokoll, Router) statt je Posten; Kunden entstehen automatisch aus der Mandanten-ID der Berichte. | `where` braucht den Ort (wohin springen, wo vorher); Posten für die Abrechnung lassen sich daraus ableiten; der Mandant ist durch die Einladung auf dem Server verlässlich, also keine Nachpflege. |
| 2026-10-18 | **6d umgesetzt:** Posten = Orte je (Mandant, Protokoll, Router) zusammengefasst; Beginn voller Monat (Zuordnung verschiebt), Ende bis Ultimo mit tagesgenauer Gegenrechnung als Hinweis; Klasse Zuordnung > Produkt > Pauschale > nicht zugeordnet; Preise optional, Schattenpreis für free/internal; Pauschale als eigene Zeile; CSV mit BOM, Semikolon, Dezimalkomma. | Deckt sich mit der Praxis (Lexware, Pauschale + Zusatz-Services); Lücken (kein Kunde, kein Produkt, keine Zahlweise) werden sichtbar statt still verschluckt. |
| geplant | Geordneter Zertifikatswechsel; optionale Root-CA. (Signierte Server-Events: umgesetzt.) | Zweite Sicherheitsschicht neben dem TLS-Pin; Betrieb ohne Aussperren. |
---
@@ -889,6 +980,14 @@ services:
* **Registry mit Vanity-URL:** `zot.chmln.de` ist noch nicht weiter getrieben (Image-Name unabhängig vom Registry-Host, z. B. `cr.<domain>/sultan`).
* **Geparkt:** Veröffentlichung auf GitHub (Quellen, Releases, ggf. ghcr.io-Spiegel).
**Schritt 6 – Accountant (5.3)** – 6a/6b ✅ 2026-10-16, 6c ✅ 2026-10-17, 6d/6e ✅ 2026-10-18
* ✅ 6a Agent: Zusatzdaten (Compose-Projekt/-Verzeichnis, Startzeit, Health; FQDN, OS, Docker-Version, NetBird-IP) im `service_state`, automatisch erkannt.
* ✅ 6b `sultan accountant`: Datenverzeichnis, Zertifikat, `accountant.db`, Enrollment-Endpunkt, `invite`, `payee list`/`delete`.
* ✅ 6c Payee: `--accountant-token`/`--accountant`, Enrollment, Schnappschuss bei Änderung (Prüfintervall 10 s) und stündlich; Accountant speichert den letzten Bericht.
* ✅ 6d Accountant (2026-10-18): Schritt 1 Historie (Zeiträume je Ort), `where`, `routes`; Schritt 2 Kunden + Zuordnung (automatisch aus der Mandanten-ID); Schritt 3 Katalog + `assign`; Schritt 4 Monats-CSV. Inhalt: Pauschale, Klassen (`included`/`billable`/`free`/`internal` mit Schattenpreis), Gruppe + Notiz je Kunde, Produktkatalog und optionale Preise mit Gültig-ab, Zahlweise je Kunde, Monats-CSV, tagesgenaue Kündigungs-Gegenrechnung. Fachfragen geklärt 2026-10-18 (Beginn voller Monat ab dem 1., manueller späterer Beginn je Posten; Zahlweise je Kunde; Abo-Abgleich manuell; Preise optional; Kostenverursacher-Gruppe + Notiz je Kunde).
* ✅ 6e Checkmk-Ausgabe per Vorlage (Default eingebaut, `dump-checkmk`) mit Links; dazu Rate-Limit für `/enroll` des Accountants (2026-10-18).
* Später: HTTP-API (Dokuwiki-Export), Mandanten-Statusberichte.
**Weitere offene Bausteine (ohne feste Reihenfolge):**
* **Lastverteilung einer Domain über mehrere Knoten** (gemeinsamer Load-Balancer statt getrennter Router) – Voraussetzung dafür, dass `drain` seine Stärke ausspielt.
* Admin-API (nur kommerziell): Mandanten, Lizenzen, Zahlungsstatus für Rechnungssystem und Ansible; Authentifizierung per API-Token, TLS über ACME.