# Changelog

Alle nennenswerten Änderungen an Clusev. Format nach
[Keep a Changelog](https://keepachangelog.com), Versionierung nach
[SemVer](https://semver.org).

**Release-Prozess:** Feature-Commits sammeln sich auf dem Branch. Ist ein Stand
reif, wird er nach `main` gemerged, als `vX.Y.Z` getaggt und hier unter einer
eigenen Versionsüberschrift dokumentiert. Nutzer beziehen ausschließlich
getaggte Releases (Kanal `stable`) — niemals Entwicklungs-Builds.

**Was hier steht:** nur, was Nutzer der Installation merken — knapp, ein Eintrag
pro Änderung, gruppiert nach _Hinzugefügt / Geändert / Behoben / Sicherheit_. Rein
interne Arbeit (Wartungs-Tooling, CI, Refactorings, Tests) gehört nicht hierher,
sondern in die git-Historie.

## [Unreleased]

_Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `vX.Y.Z` getaggt._

## [0.11.0] - 2026-07-05

### Geändert
- **Ein einziger stabiler Release-Kanal.** Nutzer erhalten ausschließlich finalisierte `vX.Y.Z`-Releases;
  die interne Release-Steuerung schneidet jetzt Release Candidates statt Vorab-Kanälen.

## [0.11.0-rc1] - 2026-06-26

### Hinzugefügt
- **Web-Terminal.** Vollwertiges SSH-Terminal direkt im Browser — eines pro Server und eines für den
  Clusev-Host selbst. Echtes PTY mit Tab-Autovervollständigung; ein eigener Reiter im Menü.
- **Geführte Einführungs-Tour.** Beim ersten Login zeigt ein Spotlight-Rundgang die wichtigsten
  Bereiche. Überspringbar und jederzeit in den Einstellungen erneut startbar.
- **Server-Suche im Terminal.** Ab mehreren Servern lässt sich die Zielliste durchsuchen.
- **Eigenes Passwort beim Anlegen eines Kontos.** Beim Hinzufügen eines Admins kann direkt ein
  Passwort vergeben werden (leer = sicheres Einmal-Passwort, das einmalig angezeigt wird).

### Geändert
- **Passwortwechsel ist optional.** Der erste Login erzwingt kein neues Passwort mehr — ein
  Warnbanner erinnert daran, sperrt aber nicht aus. 2FA war und bleibt optional (nur die
  Login-Texte waren irreführend und wurden korrigiert).
- **Passwort-Mindestlänge auf 6 Zeichen** gesenkt, ohne Komplexitätszwang — die Passwortstärke
  liegt in der Verantwortung des Nutzers.
- **Zufälliges Erst-Passwort.** Der Installer erzeugt ein zufälliges Einmal-Passwort statt des
  festen `clusev` und zeigt es nur einmal an.
- **Installer fragt nicht mehr nach der Domain.** Die Installation läuft über IP/HTTP; die Domain
  (mit automatischem HTTPS) wird später im Dashboard unter System gesetzt.
- **2FA-Status in der Seitenleiste** als dezentes Schild-Symbol statt Text-Badge.

### Sicherheit
- **Host-Key-Pinning** für die Terminal-SSH-Verbindungen — eine geänderte Server-Kennung (möglicher
  MITM) bricht die Verbindung ab, bevor Zugangsdaten übertragen werden.
- **Proxy-Vertrauen gehärtet:** nur noch private Netze werden vertraut, damit ein gefälschter
  `X-Forwarded-For` die IP-basierten Brute-Force-Limits nicht mehr umgehen kann.

## [0.10.0-rc1] - 2026-06-25

### Hinzugefügt
- **Verlaufs-Graf auf der Server-Detailseite.** CPU, RAM und Disk über Zeit — Zeitraum wählbar
  (1 h / 24 h / 7 d / 30 d), mit Hover-Tooltip; der gewählte Zeitraum bleibt in der URL erhalten.
- **SSH-Schlüssel benennen.** Beim Hinzufügen eines Schlüssels lässt sich ein Name vergeben, sodass
  Schlüssel in der Liste zuordenbar sind.

### Behoben
- **Aussperrschutz beim SSH-Root-Login.** Solange Clusev sich als `root` verbindet, lässt sich
  „SSH-Root-Login deaktivieren" nicht mehr ausführen — das hätte den eigenen Zugang gekappt.
- **fail2ban startet zuverlässig.** Die `sshd`-Jail liest jetzt das systemd-Journal statt
  `/var/log/auth.log` — so läuft fail2ban auch auf minimalen Images ohne rsyslog an.

## [0.9.68] - 2026-06-23

### Intern
- **Release-Tooling im Dev-Dashboard + CI-Pipeline.** Release Candidate schneiden, Live-Pipeline-Status,
  Veröffentlichen / Promote-to-Stable / Yank — für Nutzer der Installation nicht sichtbar.
  Details in der git-Historie. (Sammelt die internen Versionen 0.9.59–0.9.68.)

## [0.9.58] - 2026-06-22

### Hinzugefügt
- **Update-Check unterstützt jetzt GitHub.** Public-Nutzer sehen Updates aus dem öffentlichen
  GitHub-Repo (Tags + Changelog); interne Dev/Staging-Stufen bleiben privat.

### Geändert
- **Öffentliche Projekt-Adresse ist jetzt `github.com/clusev/clusev`.** README, Quelltext- und
  Konsolen-Branding sowie die Standard-Update-Quelle zeigen aufs öffentliche Repo.

## [0.9.57] - 2026-06-22

### Hinzugefügt
- **Stabiler Release-Kanal `stable`.** Nutzer beziehen ausschließlich stabile Releases (`vX.Y.Z`) —
  niemals Entwicklungs-Builds.

## [0.9.56] - 2026-06-22

### Hinzugefügt
- **Branding im Quelltext + in der Browser-Konsole.** Wer den Seitenquelltext ansieht, findet jetzt
  einen freundlichen Clusev-Hinweis (wer wir sind, Projektlink) als HTML-Kommentar. Öffnet jemand die
  Browser-Konsole, erscheint ein gestyltes „CLUSEV"-Branding mit Projektlink — **und eine
  Self-XSS-Sicherheitswarnung**: bei einem Server-Control-Panel ist „Füg das mal in deine Konsole ein"
  ein realer Social-Engineering-Angriff, daher der Hinweis genau dort. Erscheint auf jeder Seite (auch
  vor dem Login).

### Behoben
- **Datei-Manager: Dateiname im Tablet-Modus vom Rechte-Block überdeckt.** Die Tabellenspalten
  (Rechte/Größe/Geändert) + Aktions-Buttons quetschten die Namensspalte auf nahezu null. Die Liste
  rendert jetzt **unterhalb großer Bildschirme als gestapelte Karte** (Name in voller Breite, Meta-Zeile
  und Aktionen darunter); die Tabellenansicht erscheint erst auf breiten Displays. Die mobile Ansicht
  wurde dabei mit aufgeräumt.
- **Zeiger-Cursor auf Buttons fehlte.** Tailwind v4 stellt Buttons standardmäßig auf den
  Standard-Cursor; auf anklickbaren Elementen (z. B. Ordnernamen, alle Buttons) erscheint jetzt wieder
  der Hand-Zeiger.

### Behoben
- **Datei-Manager: „undefined is not valid JSON" beim Öffnen mancher Dateien.** Öffnete man eine
  **Binär-/Nicht-UTF-8-Datei**, landeten deren Rohbytes in einer Livewire-Eigenschaft. Livewire
  JSON-kodiert seinen Zustand, und ungültiges UTF-8 lässt `json_encode` scheitern → der Client bekam
  eine leere Antwort und warf den Fehler. Binär-/zu-große Dateien werden jetzt nicht mehr als Inhalt
  gebunden (es erscheint der Binär-Hinweis), sodass der Zustand immer gültiges JSON bleibt — sowohl
  im Editor als auch in der zugrunde liegenden Datei-Lese-Funktion abgesichert.

### Behoben
- **JavaScript-Fehler im Datei-Manager (und anderen Listen): „pending is not defined".** In Listen
  ohne stabilen `wire:key` ordnete Livewires DOM-Abgleich die Zeilen beim Neu-Rendern nach Position
  zu und verlor dabei den Alpine-Zustand der Buttons (Modal-Auslöser) — die Konsole warf „pending is
  not defined" und Aktionen klemmten. Alle betroffenen Listen haben jetzt einen stabilen `wire:key`:
  Datei-Manager (Einträge + Pfad), Dienste, Release-Kanal-Auswahl, Benutzer, Sicherheitsschlüssel,
  IP-Sperren sowie die Server-Detailseite (Härtung, Firewall-Regeln, SSH-Schlüssel, fail2ban-Ausnahmen).

### Behoben
- **Audit-Panel auf dem Dashboard zeigte noch Roh-Codes.** Die „letzten Ereignisse" auf dem Dashboard
  wurden noch als `wg.set-endpoint` / `deploy.update_request` angezeigt (die /audit-Seite war bereits
  lesbar). Jetzt zeigt auch das Dashboard die verständlichen Bezeichnungen, und fehlgeschlagene /
  sicherheitsrelevante Ereignisse erscheinen rot mit Warn-Symbol — konsistent mit der Audit-Seite.

### Behoben
- **„Was ist neu" wurde nicht angezeigt.** Der Vorschau-Block lud die `CHANGELOG.md` am Tag `0.9.50`,
  die Release-Tags heißen aber `v0.9.50` — der Abruf lief ins Leere (404) und der Block blieb leer.
  Jetzt wird die richtige `v`-präfixierte Tag-Referenz verwendet; die Änderungen der verfügbaren
  Version erscheinen wieder vor dem Update.

### Hinzugefügt
- **Update-Hinweis in der Seitenleiste.** Ist eine neue Version verfügbar, zeigt der Menüpunkt
  **Version** jetzt ein kleines **„1"-Abzeichen** — man sieht ein Update also auf jeder Seite, nicht
  erst auf der Version-Seite. Der Status wird von einer geplanten Prüfung (alle 30 Min, anonym und
  nur lesend gegen den Git-Host) im Cache warm gehalten; die Seitenleiste macht selbst keinen Netzabruf.

### Geändert
- Die Release-Prüfung (Version-Seite, Seitenleisten-Abzeichen, geplante Prüfung) nutzt jetzt einen
  gemeinsamen `ReleaseChecker`-Dienst (vorher in der Version-Seite verkapselt).

## [0.9.50] - 2026-06-21

### Hinzugefügt
- **Audit-Log mit Seiten-Navigation.** Das Log war auf die letzten 50 Einträge begrenzt; jetzt ist es
  paginiert (25 pro Seite), die aktuelle Seite steht in der URL (`?page=`) und ist verlinkbar.

### Geändert
- **Audit-Suche durchsucht den gesamten Verlauf.** Die Filterung läuft jetzt in der Datenbank über
  alle Einträge (nicht nur die geladene Seite) — über Akteur, Aktion, Ziel, Server **und** die
  lesbaren Bezeichnungen (z. B. findet „Endpoint geändert" die `wg.set-endpoint`-Einträge).

### Hinzugefügt
- **Was ist neu — vor dem Update sichtbar.** Ist eine neue Version verfügbar, zeigt die Version-Seite
  jetzt ein „Das ist neu in vX.Y.Z"-Feld mit dem Änderungsprotokoll der **verfügbaren** Version(en) —
  geladen aus der `CHANGELOG.md` des neuesten Release-Tags. So sieht man **vor** dem Aktualisieren,
  was sich ändert, und muss das Update nicht erst durchführen. (Anonym + nur lesend, kurz gecacht;
  bei fehlender Verbindung bleibt der Update-Knopf unverändert nutzbar.)

### Geändert
- **Audit-Log lesbar.** Statt der rohen Aktions-Codes (z. B. „Administrator · wg.set-endpoint") zeigt
  das Audit-Log jetzt verständliche Beschreibungen („Administrator · WireGuard-Endpoint geändert").
  Alle Aktionen sind übersetzt (DE/EN); unbekannte/dynamische Codes werden sauber aufbereitet. Die
  Suche findet auch die lesbaren Bezeichnungen.

### Hinzugefügt
- **Fehler im Audit-Log sichtbar.** Fehlgeschlagene oder sicherheitsrelevante Ereignisse (fehlge­schlagene
  Anmeldung, IP-Sperre, fehlgeschlagene 2FA …) werden jetzt **rot mit Warn-Symbol** hervorgehoben.
  Zusätzlich landen **fehlgeschlagene WireGuard-Aktionen** mit der Host-Fehlermeldung im Audit-Log
  (statt nur als kurzer Hinweis), sodass man später nachlesen kann, was schiefging.

### Geändert
- **Endpoint-Feld ist jetzt „nur Host".** Es wirkte, als müsse man den Port zweimal eintragen (einmal
  als Listen-Port, einmal im Endpoint). Tatsächlich genügt der Host: Label heißt jetzt „Öffentlicher
  Endpoint (Host)", der Platzhalter zeigt nur den Host (ohne `:Port`), und der Hinweis erklärt, dass
  der Port automatisch vom Listen-Port kommt. Ein abweichender Port ist nur bei Portweiterleitung
  nötig (`host:port`). Funktion war seit 0.9.43 schon so — jetzt ist es auch in der Oberfläche klar.

### Behoben
- **Traffic-Kopfzeile zeigte 0 B, obwohl der Peer Traffic hatte.** „Empfangen/Gesendet" oben speiste
  sich aus dem Sample-Durchsatz (0, solange wenig Traffic floss), während die Peer-Zeile den
  Live-Zähler zeigte — inkonsistent. Die Kopfzeile zeigt jetzt die **Live-Summe der Peer-Zähler**
  (passt damit immer zur Peer-Zeile). Das Diagramm darunter bleibt der zeitfenster-bezogene Durchsatz
  (füllt sich über den minütlichen Sampler, sobald spürbarer Traffic fließt).

### Geändert
- **Klarstellung QR-Import am Handy.** Der Hinweis sagt jetzt eindeutig: die WireGuard-App fragt beim
  QR-Scan **immer** nach einem Tunnel-Namen (App-Vorgabe, nicht abschaltbar). Für einen automatischen
  Namen die `.conf` **herunterladen** und importieren (Dateiname = Tunnel-Name).

### Hinzugefügt
- **DNS individuell einstellbar.** Der DNS-Server in den Client-Configs war fest auf `1.1.1.1`. Jetzt
  in der Server-Konfiguration frei wählbar (z. B. dein eigenes Gateway `10.0.0.1`, mehrere
  kommagetrennt) — gilt für künftige Peer-Configs, host-seitig validiert (nur IPv4). Auch per CLI:
  `clusev wg set-dns <ip[,ip]>`. Bestehende Tunnel bekommen den Wert beim ersten Setzen ergänzt.
- **WireGuard-Seite in Tabs aufgeteilt.** Zwei unabhängige Reiter: **Peers** (Liste, Anlegen,
  Entfernen, Traffic) und **Server-Konfiguration** (Endpoint, DNS, Port, Subnetz, Gate, SSH-Sperre +
  Server-Identität). Der aktive Tab ist deep-linkbar (`?tab=server`). So lassen sich Server-Einstellungen
  getrennt von der Peer-Verwaltung ändern.

### Geändert
- **Offline-Erkennung schneller.** Das Handshake-Fenster für „online" wurde von 180 s auf 150 s
  gesenkt — ein abgeschalteter Peer wird ~2,5 statt ~3 min nach dem Trennen offline (WireGuard ist
  verbindungslos, schneller geht es ohne Flackern aktiver Peers nicht).
- **Endpoint-Hinweis klargestellt:** der Port ist optional (fehlt er, wird der Listen-Port ergänzt) —
  man muss ihn nicht doppelt zum Listen-Port eintragen.

### Geändert
- **Peer-QR-Code kompakter.** Nach der Vergrößerung in 0.9.42 war der QR zu groß; er ist jetzt auf
  ein ausgewogenes Maß (~240 px) reduziert — weiterhin pixelgenau (`crispEdges`) auf weißem Rahmen,
  damit das Scannen am Handy zuverlässig bleibt.
- **Download-Dateiname als Tunnel-Name.** Die heruntergeladene Client-Config heißt jetzt
  `<endpoint-host>-<peer>.conf` (z. B. `203.0.113.10-laptop.conf`) statt `clusev-wireguard.conf`.
  WireGuard-Apps benennen einen Tunnel nach der Datei, sodass der Tunnel sinnvoll vorbenannt ist.
  (Beim QR-Scan fragt die WireGuard-App selbst nach einem Namen — das gibt der QR-Standard nicht her;
  ein Hinweis im Einmal-Dialog erklärt beide Wege.)

### Behoben
- **Endpoint ohne Port machte jede Client-Config ungültig.** Wurde beim Einrichten/Ändern nur ein
  Host ohne Port eingegeben (z. B. `203.0.113.10` statt `203.0.113.10:51820`), landete dieser
  Wert unverändert in den Peer-Configs — WireGuard verlangt aber zwingend `host:port`, sodass die
  WireGuard-App den (QR- oder Datei-)Import als „keine gültige WireGuard-Konfiguration" ablehnte.
  Der Listen-Port wird jetzt automatisch ergänzt, wenn der Endpoint keinen Port enthält (sowohl bei
  der Einrichtung als auch beim späteren Ändern). Bestehende Konfigurationen: einmal den Endpoint
  unter Einstellungen neu speichern (oder mit Port angeben), dann einen neuen Peer anlegen.

### Behoben
- **Peer-QR-Code ließ sich am Handy nicht scannen („keine gültige WireGuard-Konfiguration").** Der
  QR einer WireGuard-Client-Config ist dicht (~57 Module); er wurde im Einmal-Dialog mit nur 176 px
  angezeigt (~2,7 px pro Modul) und kantengeglättet — zu klein/unscharf, sodass die Handy-Kamera ihn
  fehlerhaft las. Der QR wird jetzt deutlich größer (bis ~300 px) auf weißem Rahmen mit Ruhezone und
  pixelgenau (`shape-rendering: crispEdges`) gerendert. Alternativ bleibt der **Herunterladen**-Knopf
  (Config als `.conf` am PC importieren).

### Behoben
- **WireGuard-Aktionen aus dem Dashboard liefen auf dem Host ins Leere („Keine Antwort vom Host").**
  Der Host-Dienst, der die Dashboard-Anfragen verarbeitet (`clusev-wg.sh serve-request`), brach unter
  `set -euo pipefail` ab, **bevor** er eine Antwort schrieb — sobald ein Feld leer oder gar nicht
  vorhanden war: ein leeres Endpoint-Feld bei der Einrichtung („auto"), oder Aktionen ohne
  bestimmte Felder (Gate ein/aus, Peer anlegen/entfernen). Dadurch wartete die Oberfläche ~30 s und
  meldete „Keine Antwort vom Host", obwohl der Watcher korrekt lief. Zwei Ursachen behoben:
  (1) die Feld-Extraktion per `grep` bricht bei fehlendem Treffer nicht mehr den ganzen Handler ab;
  (2) jede Aktion läuft jetzt in einer Subshell, sodass ein interner Abbruch (`die` bei noch nicht
  eingerichtetem Tunnel) immer eine saubere Fehlerantwort liefert statt eines Timeouts. Damit
  funktionieren Einrichtung, Peer-Verwaltung, Gate und SSH-Sperre aus dem Dashboard auf einem echten
  Host. Neuer Smoke-Test (`docker/wg/serve-request.test.sh`) sichert das ab.

## [0.9.40] - 2026-06-21

### Hinzugefügt
- **Peer-Konfiguration herunterladen.** Die Einmal-Anzeige eines neuen Clients hat neben dem
  QR-Code jetzt einen **Herunterladen**-Knopf: Die Konfiguration lässt sich als
  `clusev-wireguard.conf` speichern und am PC in die WireGuard-App importieren — alternativ zum
  Scannen des QR-Codes am Handy. Ein Hinweis erklärt beide Wege direkt in der Anzeige.
- **SSH-Sperre aus dem Dashboard (Port 22).** In den WireGuard-Einstellungen lässt sich SSH
  optional **zusätzlich** auf den Tunnel sperren — danach ist Port 22 nur noch über WireGuard bzw.
  die Server-Konsole erreichbar. Die Aktion ist mit einer deutlichen **Aussperr-Warnung** versehen
  (Inline-Hinweis **und** Bestätigungsdialog): Wer seinen WG-Zugang verliert, kommt per SSH nicht
  mehr auf den Host — Empfehlung, vorher einen zweiten WG-Peer als Backup-Zugang anzulegen. Auf das
  Dashboard kommt man auch ohne SSH weiter über den Tunnel. Bestehende SSH-Sitzungen bleiben
  verbunden; ein nicht laufender Tunnel verhindert das Aktivieren. Neue CLI-Notausgänge:
  `clusev wg ssh-lock` / `clusev wg ssh-unlock`, und `clusev wg down` (Server-Konsole) hebt alle
  Sperren auf. Die Sperre übersteht Neustarts nur bei aktivem Tunnel — kommt wg0 nicht hoch, bleibt
  SSH offen (kein Aussperren durch einen kaputten Boot).

## [0.9.39] - 2026-06-21

### Geändert
- **WireGuard-Einrichtung: SSH-Hinweis von der Seite entfernt.** Das `sudo clusev wg setup` steht
  jetzt nur noch in der Hilfe — die Einrichtung läuft über das Dashboard-Formular.

### Behoben
- **Kein ewiges „Wird angewendet …" mehr.** Antwortet der Host-Dienst nicht (z. B. weil der Stack
  noch nicht auf eine Version mit dem Host-Watcher aktualisiert wurde), bricht die Dashboard-Aktion
  nach ~30 s mit einer klaren Meldung ab, statt unbegrenzt zu warten. (Hinweis: Peer-Verwaltung +
  Einrichtung aus dem Dashboard benötigen den per `sudo clusev update` installierten Host-Watcher —
  auf einem reinen Dev-Stack ohne diesen läuft die Brücke nicht.)

## [0.9.38] - 2026-06-21

### Hinzugefügt
- **WireGuard-Erst-Einrichtung aus dem Dashboard.** Der „nicht eingerichtet"-Zustand auf
  `/wireguard` ist jetzt ein Einrichtungs-Formular (Subnetz / Port / Endpoint / erster Client):
  WireGuard lässt sich **vollständig aus der UI** aufsetzen, ohne SSH. Der Host prüft das Subnetz
  host-seitig auf Kollisionen, erzeugt die Schlüssel, startet den Tunnel und gibt die Konfiguration
  des ersten Clients als Einmal-Anzeige (Text + QR) zurück. Das Gate bleibt dabei **aus** (kein
  Aussperren). Damit ist WireGuard zu 100 % aus dem Dashboard steuerbar — Live-Status, Traffic,
  Peers, Einstellungen, Gate **und** Erst-Einrichtung; `sudo clusev wg setup` bleibt als Alternative.

### Behoben
- Korrigiert, dass die Erst-Einrichtung in 0.9.37 fälschlich nur über die CLI möglich war.

## [0.9.37] - 2026-06-21

### Hinzugefügt
- **WireGuard-Server-Einstellungen + Gate aus dem Dashboard (SP2, Phase 4 — SP2 abgeschlossen).** Auf
  `/wireguard` lassen sich jetzt das **Gate ein-/ausschalten** (mit Bestätigung — aus = Panel wieder
  öffentlich, ein = nur über den Tunnel; SSH-Notausgang bleibt) sowie der **öffentliche Endpoint**,
  der **Listen-Port** und das **Subnetz** ändern. Port- und Subnetz-Änderungen sind destruktiv
  (Tunnel-Neustart bzw. Re-Adressierung — Clients/Peers müssen neu) und erfordern eine harte
  Bestätigung; das Subnetz wird host-seitig auf Kollisionen geprüft. Alle Mutationen laufen über die
  abgesicherte Schreib-Brücke (Whitelist + erneute host-seitige Validierung jedes Werts), sind
  auditiert und ratenbegrenzt. Damit ist WireGuard vollständig aus dem Dashboard verwaltbar:
  Live-Status, Traffic-Verlauf, Peer-Verwaltung und Server-Einstellungen.

## [0.9.36] - 2026-06-21

### Hinzugefügt
- **WireGuard-Peer-Verwaltung aus dem Dashboard (SP2, Phase 3).** Auf `/wireguard` lassen sich Clients
  jetzt **anlegen** (Name eingeben → die Client-Konfiguration + QR-Code erscheinen **einmalig** in
  einem Dialog und werden nie gespeichert) und **entfernen** (mit Bestätigung). Umgesetzt über eine
  neue, abgesicherte **Schreib-Brücke**: die App schreibt eine validierte Anfrage nach
  `run/wg-request.json`, ein Host-Watcher (`clusev-wg.sh serve-request`) prüft Aktion + Argumente
  **erneut host-seitig** (Whitelist, strikte Muster — Container-Eingaben sind Daten, nie Code),
  führt den festen Befehl als root aus und schreibt ein Ergebnis (0600, app-eigen, nach 60 s gelöscht).
  Jede Aktion ist auditiert + pro Benutzer ratenbegrenzt.

### Sicherheit
- Ergebnis-/QR-Dateien (enthalten einen frischen privaten Schlüssel) werden via `umask 077` von Beginn
  an mit `0600` erzeugt — kein kurzes world-readable-Fenster (durch ein adversariales Security-Review
  gefunden + behoben).

## [0.9.35] - 2026-06-21

### Hinzugefügt
- **WireGuard-Traffic-Verlauf (SP2, Phase 2).** Die `/wireguard`-Seite zeigt jetzt einen
  Durchsatz-Graphen (Empfangen/Gesendet) über ein wählbares Zeitfenster (1 Std / 24 Std / 7 Tage).
  Ein periodischer Befehl (`clusev:wg-sample`, minütlich) speichert pro Peer Traffic-Stichproben in
  der Datenbank; der Graph aggregiert daraus den Durchsatz (Deltas, Zähler-Resets sauber abgefangen)
  und hält 7 Tage Historie vor. Server-seitig gerendert (SVG, kein JS-Chart) — passt zum bestehenden
  Dashboard-Chart-Stil.
- **Scheduler-Dienst.** Der Prod-Stack führt jetzt den Laravel-Scheduler aus (`schedule:work`) —
  Voraussetzung für die Traffic-Stichproben und der Auslöser für die bereits vorhandene tägliche
  Audit-Log-Bereinigung (`clusev:prune-audit`), die zuvor mangels Scheduler nie lief.

## [0.9.34] - 2026-06-20

### Hinzugefügt
- **WireGuard-Dashboard — Live-Status (SP2, Phase 1).** Neue Seite `/wireguard` (Reiter „WireGuard"
  in der Flotte) zeigt den Tunnel live: verbundene Peers (Name, online/offline, letzter Handshake,
  Endpoint, ↓/↑-Traffic), Gate-Zustand (an/aus + SSH-Notausgang-Hinweis) und Server-Info (Subnetz,
  Server-IP, Port, Endpoint, Dienst). Gespeist von einem periodischen Host-Collector
  (`clusev wg collect` via systemd-Timer → `run/wg-status.json`), alle 5 s aktualisiert; ein veralteter
  Status (Collector offline) wird als solcher angezeigt. Read-only — Peer-Verwaltung + Einstellungen
  aus der UI folgen in den nächsten Phasen. Erscheint, sobald WireGuard auf dem Host via
  `sudo clusev wg setup` eingerichtet ist.

## [0.9.33] - 2026-06-20

### Hinzugefügt
- **WireGuard-Zugang (SP1) — Panel hinter einem Tunnel.** Optionaler Netzwerk-Schutz *zusätzlich* zu
  2FA und Anmeldeschutz: Der Server wird zum WireGuard-Server, und die Panel-Ports 80/443 lassen sich
  auf das WG-Subnetz beschränken — komplett über die Host-CLI
  `clusev wg setup|up|down|status|add-peer|remove-peer` (per SSH). Standardmäßig **aus**. Sicherheit:
  SSH (22) und der WG-Port sind von der Sperre nie betroffen, `clusev wg down` (per SSH) ist der
  Notausgang, und ein nach Neustart fehlgeschlagenes `wg0` lässt das Panel offen statt auszusperren.
  Die Sperre ist eine isolierte iptables-Chain (`CLUSEV-WG-GATE`) an `DOCKER-USER`; eine systemd-Unit
  wendet sie nach Boot/Docker-Neustart wieder an — nur wenn Tunnel **und** Marker aktiv sind.
  `install.sh` installiert `wireguard-tools`+`qrencode` (inert, bis der Operator `clusev wg setup`
  ausführt). Neuer Hilfe-Eintrag „WireGuard-Zugang" (DE+EN).
  *Rein host-seitig (WireGuard/Firewall laufen außerhalb des Containers). Die Shell-Skripte sind
  shellcheck-sauber und mehrfach reviewt, aber nicht durch die PHPUnit-Suite testbar — die Laufzeit
  (setup/up/down/Gate) ist per Runbook auf einer VM zu verifizieren (`docs/superpowers/runbooks/`).*

## [0.9.32] - 2026-06-20

### Hinzugefügt
- **Serie & Seite als URL-Parameter.** Die gewählte Versionsreihe und die Seite des Änderungs­
  protokolls stehen jetzt in der Adresse (`?series=0.8&page=2`) — teilbar und per Reload/Zurück-Taste
  erhalten.
- **Echter Update-Fortschritt statt Zeit-Schätzung.** Der Host-Updater meldet seine aktuelle Phase
  über eine Datei (`run/update-phase.json`); die Update-Seite liest sie und schaltet die Schritte
  **abrufen → Image bauen → Dienste neu starten → migrieren** der Reihe nach echt weiter, statt alle
  vier am Ende auf einmal grün springen zu lassen. Abschluss erfolgt über das reale „done"-Signal des
  Updaters (Fallback: Versions-Wechsel). Greift — wie der 502-Fix — erst beim Update, das von einer
  bereits mit 0.9.32 laufenden Instanz gestartet wird (die während eines Updates angezeigte Seite ist
  stets noch die der alten Version); ohne Feed bleibt die bisherige Zeit-Heuristik als Fallback.

### Geändert
- Update-Phasen in realer Reihenfolge (Dienste-Neustart vor Datenbank-Migration).

## [0.9.31] - 2026-06-20

### Geändert
- **Versions-Verlauf als Serien-Browser statt langer Liste.** Releases sind jetzt nach
  Hauptversion (major.minor, z. B. „0.9") gruppiert: eine Serien-Navigation (ab großen Bildschirmen
  eine seitliche Leiste, darunter horizontale Pills) wählt die Reihe, rechts erscheinen deren
  Releases als Accordion (das neueste offen) mit **Seitenblätterung** — kein endloses Scrollen über
  alle Versionen mehr. Die installierte Reihe ist markiert, jede Zeile zeigt die Änderungs-Kategorien
  als farbige Punkte auf einen Blick.

### Behoben
- **Aufklapp-Pfeil & „Weiter"-Knopf sichtbar.** Das `chevron-right`-Icon fehlte im Icon-Satz, sodass
  der Accordion-Pfeil jeder Zeile und der „Weiter"-Knopf der Seitenblätterung als leeres SVG
  gerendert wurden (unsichtbare Bedienelemente). Icon ergänzt.
- **Aufgeklappter Eintrag bleibt offen.** Ein selbst aufgeklapptes Release wird nicht mehr durch
  einen Hintergrund-Re-Render (Update-Prüfung) wieder zugeklappt (`wire:ignore.self`).
- **Touch-Zielgrößen.** Serien-Pills und Seiten-Knöpfe erfüllen auf kleinen Bildschirmen jetzt die
  44-px-Touch-Mindestgröße.

## [0.9.30] - 2026-06-20

### Behoben
- **Update löst keinen 502 mehr aus.** Die Fortschritts-Seite erkannte „fertig" bislang an zwei
  aufeinanderfolgenden `200` auf der Health-Route `/up` — doch der **alte** Container antwortet dort
  während des minutenlangen Neuaufbaus weiter mit `200`, sodass zu früh zurückgeleitet wurde,
  mitten in den Stack-Neustart hinein → 502/Weißbild. Die Seite pollt jetzt einen schlanken
  Versions-Endpoint (`/version.json`) und gilt erst dann als fertig, wenn die **laufende Version
  tatsächlich über die Ausgangsversion hinaus** gewechselt ist (bzw. nach einem beobachteten
  down→up-Zyklus bei gleicher Version). Erst dann erfolgt der automatische Rücksprung.

## [0.9.29] - 2026-06-20

### Geändert
- **Versions-Verlauf als Accordion.** Das Änderungsprotokoll zeigt pro Eintrag nur noch
  Versionsnummer + Datum; ältere Versionen sind eingeklappt und öffnen per Klick, die neueste ist
  immer offen — bei vielen Releases deutlich übersichtlicher.
- **„Update verfügbar"-Karte responsive entstaucht** — auf schmalen Bildschirmen stapeln Version,
  Badges, Aktion (Button volle Breite) und Prüfzeitpunkt sauber untereinander statt gequetscht.
- **Professionellere Phasen-Beschriftung** auf der Update-Fortschritts-Seite („Update wird
  abgerufen / Image wird erstellt / Datenbank wird migriert / Dienste werden neu gestartet").

### Hinzugefügt
- **Direkter Server-Link im Dashboard** — Name und IP der aktiven Maschine verlinken jetzt direkt
  auf deren Detailseite (vorher: erst „Server", dann auswählen).

## [0.9.28] - 2026-06-20

### Behoben
- **Update bricht nicht mehr mit 502 / Weißbild ab.** Beim Auslösen eines Updates erscheint jetzt
  eine eigenständige Fortschritts-Seite (`/update-progress`, ohne Livewire), die den Neuaufbau des
  Stacks im Browser übersteht: sie pollt die Health-Route, zeigt die Phasen
  (Holen/Bauen/Migrieren/Neustart) + verstrichene Zeit und leitet nach der Wiederherstellung
  automatisch dorthin zurück, wo man war. (Open-Redirect-sicher: das Rücksprung-Ziel wird auf einen
  gleich-origin Relativpfad beschränkt.)
- **Checkbox im Reiter „Anmeldeschutz"** nutzt jetzt das Design-System (Signal-Orange + sichtbarer
  Haken über eine wiederverwendbare `x-checkbox`-Komponente; ebenso „Angemeldet bleiben" im Login)
  statt der blauen Browser-Standard-Checkbox.
- **2FA-Status-Badge vereinheitlicht.** Identity-Header, Benutzer-Liste und Sidebar zeigen den
  „2FA aktiv/aus"-Status jetzt im selben Stil (eine `x-two-factor-badge`-Komponente) statt zuvor
  drei verschiedener Darstellungen.

### Hinweis
- **Security-Key (WebAuthn/YubiKey) erfordert eine Domain mit HTTPS.** Bei Bare-IP-Zugriff ohne
  konfigurierte Domain ist ein Security-Key nicht nutzbar (eine IP kann keine WebAuthn-rpId sein);
  ein Schlüssel-only-Konto wird dann korrekt auf den Backup-Code geleitet. Für den
  Security-Key-Button das Panel unter einer Domain über HTTPS betreiben.

## [0.9.27] - 2026-06-20

### Hinzugefügt
- **Anmeldeschutz — Brute-Force-Sperre für das Panel-Login selbst.** Nach zu vielen fehlgeschlagenen
  Anmelde- oder 2FA-Versuchen wird die Quell-IP zeitlich gesperrt (Standard: 10 Versuche in 10 Min →
  60 Min Sperre). Durchgesetzt auf HTTP-Ebene für **nicht angemeldete** Anfragen — eingeloggte
  Operatoren werden nie geblockt und können ihre eigene IP weiterhin im Panel entsperren. (Eigenständig
  von der Flotten-fail2ban-Verwaltung, die entfernte Server härtet.)
- **Neuer Einstellungs-Reiter „Anmeldeschutz"** (Einstellungen → Anmeldeschutz): An/Aus, max. Versuche,
  Zeitfenster, Bann-Dauer, Whitelist (IP/CIDR), Liste aktiver Sperren mit Entsperren / Alle entsperren,
  plus „Meine IP zur Whitelist hinzufügen".
- **Host-Befehl `clusev unban <ip>` (`--all`)** als Notfall-Ausweg, falls man sich aussperrt — analog
  zu `clusev reset-admin`.
- Fehlgeschlagene Logins und 2FA-Versuche sowie Sperren/Entsperrungen landen jetzt im **Audit-Log**
  (`auth.login_failed`, `auth.2fa_failed`, `auth.ip_banned`, `auth.ip_unbanned`).

### Sicherheit
- Loopback und private Netze sind standardmäßig ausgenommen (gegen Selbst-Aussperrung). Der
  IP-Vergleich erfolgt kanonisch (inet_pton; IPv4-mapped-IPv6 wird auf die IPv4-Form zusammengefasst),
  damit eine Sperre nicht über zwei Schreibweisen derselben Adresse umgangen werden kann. Sperren sind
  immer zeitlich begrenzt — es gibt keinen permanenten Lockout.

## [0.9.26] - 2026-06-20

### Hinzugefügt
- **Eigene Backup-Code-Seite im Zwei-Faktor-Login** (`/two-factor-challenge/backup`). Über den Button
  „Backup-Code verwenden" auf dem Bestätigungs-Screen erreichbar; trennt den seltenen
  Wiederherstellungs-Fall sauber vom Alltags-Login.

### Geändert
- **Der 2FA-Bestätigungs-Screen zeigt nur noch die primäre Methode** (Authenticator-Code *oder*
  Security-Key) — der Backup-Code ist nicht mehr inline neben der Eingabe, sondern auf der eigenen Seite.
- **Security-Key-only ohne Secure-Context** (bare-IP/HTTP, wo WebAuthn nicht läuft): der Login leitet
  direkt zur Backup-Code-Seite weiter, statt einen Screen ohne nutzbare Methode zu zeigen.
- Gemeinsame Challenge-Logik (Pending-User, Brute-Force-Zähler, Login-Abschluss) in einen Trait
  ausgelagert; beide Ansichten teilen denselben Topf — das Backup-Code-Rate-Limit (5/60 s pro
  Nutzer+IP, 20/900 s pro Nutzer) gilt unverändert auch auf der neuen Seite.

## [0.9.25] - 2026-06-19

### Hinzugefügt
- **Host-Befehl `clusev`** (vom Installer nach `/usr/local/bin` gelegt): kurze Wrapper für
  `update`, `reset-admin`, `restart`, `logs`, `ps`, `migrate`, `artisan`, `version`. Statt langer
  `docker compose`-Aufrufe mit der Prod-Compose-Datei genügt z. B. `sudo clusev update` oder
  `clusev reset-admin`. `clusev help` zeigt die Übersicht.
- **Hilfe-Thema „Befehle / CLI" (DE/EN):** dokumentiert jeden `clusev`-Befehl ausführlich und zeigt,
  was im Hintergrund läuft.

### Geändert
- **Hilfe-Tabs stehen in der URL** (`#[Url]` am Thema) — Reload/Lesezeichen/Teilen behalten das aktive
  Tab, wie bei den Einstellungen.
- Alle Operator-Flächen (Update-Hinweis unter „Version & Releases", Hilfe-Wiederherstellung/-Updates/
  -Domain-TLS, Reset-Hinweise in Einstellungen/System, Sentinel-Fehlermeldungen, MOTD) zeigen jetzt die
  kurzen `clusev …`-Befehle; der lange Compose-Dateiname taucht in der Oberfläche nicht mehr auf.

## [0.9.24] - 2026-06-19

### Hinzugefügt
- **In-Panel-Hilfe-Seite (`/help`, Nav „Hilfe", `g h`).** Linke Themen-Navigation wie die
  Einstellungen, zweisprachig (DE/EN, folgt dem Umschalter). Themen: Überblick, Domain/TLS &
  Reverse-Proxy, Sicherheit & 2FA, Updates & Versionen, Server & SSH, Sitzungen & Benutzer, E-Mail,
  Audit-Log, Konto-Wiederherstellung. Enthält u. a. eine **generische Schritt-für-Schritt-Anleitung
  für den Betrieb hinter einem externen Reverse-Proxy** (was im Proxy eintragen: HTTP-Ziel,
  `X-Forwarded-Proto: https`, WebSocket, `TRUSTED_PROXY_CIDR`) und den **2FA-Zugangspfad-Hinweis**
  (Security-Key nur über die HTTPS-Domain; über Bare-IP/HTTP Backup-Code nötig; TOTP überall).

### Geändert
- TLS-Modus-Hinweis nennt keinen Produktnamen mehr — „ein vorgelagerter Proxy Manager terminiert TLS".

## [0.9.23] - 2026-06-19

### Behoben
- **Konsolen-Warnung „resource was preloaded but not used".** Vite legte zu jeder Seite einen
  `<link rel="preload">` für CSS/JS zusätzlich zum normalen Stylesheet/Script an. Bei nur einem
  CSS- und einem JS-Bundle bringt der Preload nichts (die Tags stehen ohnehin direkt daneben), löste
  aber Chromes „preloaded but not used"-Warnung aus — verstärkt durch DevTools „Cache deaktivieren",
  das die CSS doppelt lädt. Die Preload-Hints werden jetzt weggelassen (`Vite::usePreloadTagAttributes(false)`);
  Stylesheet und Script laden unverändert.

## [0.9.22] - 2026-06-19

### Behoben
- **Passwortmanager (Bitwarden) kaperte die Security-Key-Registrierung.** Trotz `cross-platform`
  fing Bitwardens Browser-Erweiterung `navigator.credentials.create` ab und bot „Passkey speichern"
  an. Die Optionen enthalten jetzt zusätzlich den **WebAuthn-L3-Hint `hints: ['security-key']`** (bei
  Registrierung und Login), den moderne Browser/Manager respektieren und daher beiseite treten — der
  YubiKey wird direkt abgefragt. (Ältere Bitwarden-Versionen, die den Hint nicht kennen, kapern
  evtl. weiter — dann in Bitwarden „Nach Passkeys fragen" für die Seite/global deaktivieren.)

## [0.9.21] - 2026-06-19

### Behoben
- **Security-Key-Registrierung zielte nicht auf einen Hardware-Schlüssel** → der Browser bot das
  Passkey-Speichern (z. B. Bitwarden) an statt den YubiKey abzufragen. Die Registrierung setzt jetzt
  `authenticatorSelection`: **cross-platform** (externer/roaming Schlüssel statt Plattform-/
  Passwortmanager-Passkey), **residentKey discouraged** (nicht-auffindbarer Zweitfaktor — genau das,
  was die Passkey-Manager-UI vermeidet) und **userVerification discouraged** (nur Anwesenheit = ein
  Tippen, kein PIN/Biometrie). Login fragt den Schlüssel ebenfalls mit „discouraged" ab → **einstecken,
  einmal tippen, fertig**.

## [0.9.20] - 2026-06-19

### Behoben
- **Security-Keys (WebAuthn/YubiKey) waren hinter einem externen TLS-Proxy gesperrt.** Die
  Verfügbarkeit hing an `request()->isSecure()` — das im Externer-Proxy-Modus `false` ist (der
  weitergereichte HTTPS-Scheme wird ohne `TRUSTED_PROXY_CIDR` nicht vertraut), obwohl das Panel
  über die Domain per HTTPS läuft. Security-Keys werden jetzt freigeschaltet, sobald das Panel über
  die **aktive Domain** erreicht wird (immer HTTPS über die Vordertür), unabhängig vom hier
  erkannten Request-Scheme. Der Bare-IP-Recovery-Pfad (kein Secure-Context) bleibt korrekt gesperrt.
  Ein Kern-Sicherheitsfeature hängt damit nicht mehr an einer optionalen Proxy-Einstellung.

## [0.9.19] - 2026-06-19

### Behoben
- **Caddyfile-Änderungen wurden bei einem Update nicht übernommen.** Die Caddy-Config war als
  **einzelne Datei** gemountet; ein `git pull` ersetzt die Datei (neue Inode), aber der laufende
  Container hängt weiter an der alten — und `docker compose up -d` baut Caddy bei reiner
  Inhaltsänderung nicht neu. Dadurch kamen Caddy-Fixes (z. B. der WebSocket-Fix aus 0.9.18) ohne
  manuelles Eingreifen nie an. Jetzt wird das **Verzeichnis** `docker/caddy` gemountet (spiegelt die
  Datei live) und `install.sh` baut den Caddy-Container bei jedem Deploy gezielt neu
  (`up -d --force-recreate caddy`), sodass Caddyfile-Änderungen zuverlässig wirksam werden.

## [0.9.18] - 2026-06-19

### Behoben
- **Realtime (WebSocket) hinter einem externen TLS-Proxy lief nicht.** Der Echo/Reverb-Endpunkt
  wurde aus dem (hinter dem Proxy als `http:80` erscheinenden) Request abgeleitet → `wss://host:80`,
  was scheiterte. Bei aktiver Domain wird der Reverb-Client jetzt fest auf **`wss://<domain>:443`**
  (die HTTPS-Vordertür) gesetzt; der Bare-IP-Recovery-Pfad bleibt einfaches `ws`. Zusätzlich leitet
  Clusevs Caddy die WS-Pfade (`/app/*`, `/apps/*`) **nicht mehr auf https um, wenn der Upstream
  bereits TLS terminiert hat** (`X-Forwarded-Proto: https`) — sonst brach der Redirect die
  WebSocket-Verbindung im Externer-Proxy-Modus.
- **TLS-Status zeigte im Externer-Proxy-Modus fälschlich „Let's Encrypt".** Die „TLS aktiv"-Karte
  meldet jetzt korrekt „TLS aktiv — über externen Reverse-Proxy; das Panel stellt kein Zertifikat
  aus" statt einer automatischen Let's-Encrypt-Ausstellung, die in diesem Modus nicht stattfindet.

## [0.9.17] - 2026-06-19

### Behoben
- **Panel über die Domain hinter einem externen TLS-Proxy war unbenutzbar (CSS/JS per CSP
  blockiert).** Im Externer-Proxy-Modus terminiert der Upstream HTTPS und reicht das Schema per
  `X-Forwarded-Proto` weiter; Clusevs eigener Caddy vertraut dem aber nur, wenn `TRUSTED_PROXY_CIDR`
  gesetzt ist. War es das nicht, sah die App `http`, erzeugte `http://`-Asset-URLs — und die über
  HTTPS ausgelieferte Seite (CSP `'self'`) blockierte diese → kein CSS/JS, kein Login. Bei aktiver
  Domain wird die URL-Erzeugung (Assets, Routen) jetzt fest auf `https://<domain>` gezwungen
  (`URL::forceRootUrl`), unabhängig vom (evtl. nicht vertrauten) Request-Schema. Der Bare-IP-
  Recovery-Pfad (HTTP) bleibt unberührt. Hinweis: für korrekte `Secure`-Cookies im Externer-Proxy-
  Modus weiterhin `TRUSTED_PROXY_CIDR` auf die Proxy-Adresse setzen.

## [0.9.16] - 2026-06-19

### Behoben
- **Neustart-Knopf meldet einen fehlgeschlagenen Sentinel-Schreibvorgang** — konsistent mit dem
  Update-Knopf (0.9.15). Konnte die Sentinel-Datei nicht geschrieben werden (z. B. bei einem
  uid-Mismatch vor dem Fix), zeigte „Jetzt neu starten" nur „Neustart wird ausgeführt …" ohne dass
  etwas passierte; jetzt erscheint stattdessen ein Fehler-Toast mit dem Hinweis auf `sudo ./update.sh`.
  `DeploymentService::requestRestart()` gibt nun `bool` zurück.

## [0.9.15] - 2026-06-19

### Behoben
- **Dashboard-Update/-Neustart hingen auf Hosts, deren `clusev`-Nutzer nicht uid 1002 hat.** Das
  Prod-Image baute den `app`-Nutzer fest mit uid 1002, während `install.sh` das bind-gemountete
  `./run` dem `clusev`-Nutzer (z. B. uid 1000) übergibt. Dadurch konnte der App-Prozess die
  Sentinel-Datei nicht schreiben — der Knopf zeigte „läuft", aber auf dem Server passierte nichts.
  Das Image wird jetzt mit `APP_UID/APP_GID = HOST_UID/HOST_GID` (dem `clusev`-Nutzer) gebaut, sodass
  App-Prozess und `./run` übereinstimmen. **Einmalig per `sudo ./update.sh` neu bauen**, danach
  funktioniert der Knopf (der konnte sich nicht selbst reparieren, da genau das Schreiben kaputt war).
- **Fehlgeschlagenes Sentinel-Schreiben wird jetzt gemeldet** statt eines ewigen „läuft": geht das
  Schreiben schief, erscheint ein Fehler-Toast mit dem Hinweis auf `sudo ./update.sh`.

## [0.9.14] - 2026-06-19

### Geändert
- **Updates werden beim Öffnen der Seite automatisch geprüft** (System → Version & Releases). Bisher
  zeigte nur das Badge eine neuere Version; der „Jetzt aktualisieren"-Knopf erschien erst nach Klick
  auf „Nach Updates suchen". Die Seite prüft jetzt direkt beim Laden selbst (asynchron via
  `wire:init`, blockiert das Rendern nicht) und zeigt ein verfügbares Update samt Knopf sofort an —
  kein manuelles Suchen mehr. Während eines laufenden Updates wird der Auto-Check übersprungen, damit
  er den „läuft"-Zustand nicht überschreibt.

## [0.9.13] - 2026-06-19

### Behoben
- **Update-Status übersteht jetzt einen Reload (in Redis statt Sentinel gespeichert).** In 0.9.12
  wurde der laufende Zustand aus der Sentinel-Datei abgeleitet — die der Host-Watcher aber **vor**
  dem Update verbraucht, sodass ein Reload mitten im Update nichts mehr anzeigte. Der „läuft"-Status
  wird jetzt als Redis-Marker (mit der Ausgangsversion) persistiert: er übersteht sowohl den Reload
  als auch den Container-Neustart. Nach einem Reload wird entweder „Update läuft" wieder aufgenommen
  (inkl. Polling) oder — falls das Update inzwischen durch ist — direkt „Aktuell — vX" mit
  Erfolgsmeldung angezeigt und der Marker gelöscht (15-Min-Backstop-TTL, damit nichts hängen bleibt).

## [0.9.12] - 2026-06-19

### Hinzugefügt
- **Fertig-Rückmeldung beim Dashboard-Update.** Nach „Jetzt aktualisieren" zeigte die Seite nur
  „Update läuft" ohne Ende. Sie pollt jetzt (alle 5 s) und erkennt den Abschluss daran, dass der
  neu gebaute Container eine höhere Version meldet als beim Start des Updates — dann springt die
  Karte automatisch zurück auf „Aktuell — vX" und ein Erfolgs-Toast erscheint. Übersteht die
  Neustart-Downtime (fehlgeschlagene Polls werden einfach wiederholt) und bricht nach ~5 Min mit
  einem Hinweis ab, falls das Update hängt — statt ewig zu drehen. Ein laufendes Update wird auch
  nach einem Seiten-Reload wieder aufgenommen (solange die Sentinel-Datei besteht).

### Geändert
- **Verfügbares Update wird sofort beim Laden angezeigt.** Stand der neueste Tag bereits aus einer
  vorherigen Prüfung im Cache, zeigte nur das Badge die neue Version — der „Jetzt aktualisieren"-
  Knopf erschien aber erst nach erneutem „Nach Updates suchen". Jetzt erscheint er direkt beim
  Seitenaufbau (ohne Netzwerkanfrage — rein aus dem Cache).

### Behoben
- **Zeitstempel in lokaler Zeit statt UTC.** `app.timezone` ist jetzt env-gesteuert; `install.sh`
  ermittelt die Zeitzone des Hosts (systemd / `/etc/timezone` / `/etc/localtime`) und schreibt sie
  als `APP_TIMEZONE` in die `.env`. Datenbank-Zeitstempel bleiben UTC; nur die Anzeige (z. B. die
  „geprüft"-Zeit) folgt der Host-Zeitzone. Default UTC, wenn nicht ermittelbar.

## [0.9.11] - 2026-06-19

### Behoben
- **„Neuestes Release" zeigt nie mehr eine ältere Version als die installierte.** Direkt nach einem
  Update konnte der 30-Minuten-Cache der Update-Prüfung noch den vorherigen Stand halten, sodass die
  Karte „Installiert" z. B. „Neuestes Release v0.9.9" neben „installiert v0.9.10" zeigte (wirkte
  rückwärts). Ist der gecachte Wert kleiner als die installierte Version, wird er jetzt verworfen
  (Anzeige „—"), bis „Nach Updates suchen" den echten neuesten Tag auflöst.

## [0.9.10] - 2026-06-19

### Behoben
- **Build-Commit & Branch werden jetzt auch in Produktion angezeigt** (System → Version & Releases,
  Karte „Installiert"). Sie wurden zur Laufzeit aus `.git` gelesen — das im Docker-Prod-Image
  fehlt — also stand dort „Build —" und als Branch der Kanal. `install.sh` bäckt den deployten
  Commit (`git rev-parse --short HEAD`) und Branch jetzt aus dem `.git` des Hosts in die `.env`
  (`CLUSEV_BUILD_SHA`/`CLUSEV_BUILD_BRANCH`); die Versions-Seite bevorzugt diese und liest im Dev
  weiterhin direkt aus `.git`.

## [0.9.9] - 2026-06-19

### Hinzugefügt
- **„Jetzt aktualisieren" im Dashboard (System → Version & Releases).** Ist ein neueres Release im
  Kanal verfügbar, lässt sich das Update jetzt per Knopf auslösen — ganz ohne SSH. Gleiches sichere
  Muster wie der Neustart-Knopf: der Container schreibt nur eine Sentinel-Datei (kein Docker-Socket),
  ein **als root laufender** Host-Watcher (`clusev-update.path`/`.service`) führt `update.sh` aus
  (`git pull` + idempotenter Re-Install) und verbraucht die Sentinel **vor** dem Lauf, damit ein
  dauerhafter Fehler nicht endlos wiederholt. Der Knopf erscheint nur, wenn eine neuere Version
  gefunden wurde, ist per-Nutzer gedrosselt (3/10 Min, auto-ablaufend) und auditiert
  (`deploy.update_request`). Hinweis im UI: das Dashboard ist während des Rebuilds ein bis zwei
  Minuten nicht erreichbar; danach neu laden. `install.sh` installiert die neuen Host-Units mit.

## [0.9.8] - 2026-06-19

### Hinzugefügt
- **`update.sh` — Ein-Befehl-Update (`sudo ./update.sh`).** Holt den neuesten Stand
  (`git pull --ff-only`, verwirft nie lokale Änderungen), **aktualisiert sich bei Bedarf selbst**
  (hat sich `update.sh` im Pull geändert, startet es die neue Version neu) und übergibt dann an den
  idempotenten `install.sh` (Build, Neustart, Migration, MOTD). Setzt für root automatisch
  `safe.directory` (das Verzeichnis gehört dem `clusev`-Nutzer, sonst verweigert ein root-`git pull`)
  und ruft `install.sh` **nicht-interaktiv** auf, sodass nichts neu abgefragt wird.

### Behoben
- **Re-Run von `install.sh` bewahrt jetzt Domain & ACME-E-Mail.** Bislang schrieb der Installer
  `APP_DOMAIN`/`ACME_EMAIL` bei jedem Lauf — ein nicht-interaktiver Re-Run (also ein Update) ohne
  gesetzte `CLUSEV_DOMAIN` hätte eine konfigurierte Domain **gelöscht**. Beide defaulten jetzt auf den
  bestehenden `.env`-Wert; Erst-Installation (leer = Bare-IP) unverändert.

### Geändert
- **README-Update-Abschnitt** auf `sudo ./update.sh` umgestellt (der alte Zweischritt
  `git pull && sudo ./install.sh` funktioniert weiterhin); MOTD zeigt den Update-Befehl an.

## [0.9.7] - 2026-06-19

### Behoben
- **Update-Prüfung (System → Version & Releases) funktioniert jetzt in Produktion.** Sie las das
  neueste Release aus dem lokalen `.git` — das es im Docker-Prod-Image gar nicht gibt, daher meldete
  „Nach Updates suchen" dort **immer** „Noch kein Release getaggt", selbst wenn längst eine neuere
  Version veröffentlicht war. Der neueste Tag im Kanal wird jetzt über die **öffentliche, lesende
  Tag-API des Git-Hosts** abgefragt (anonym — kein Token verlässt das Panel), mit lokalem `.git` nur
  noch als Dev-Fallback; das Ergebnis ist gecacht, sodass ein Seitenaufruf nie eine Netzwerkanfrage
  auslöst (nur der Knopf selbst). Kanal-Logik: `stable` bietet ausschließlich `vX.Y.Z` an.
  Schlägt die Abfrage fehl (offline o. Ä.), degradiert sie still statt zu blockieren.

## [0.9.6] - 2026-06-19

### Geändert
- **Erst-Admin nutzt jetzt eine feste Standard-E-Mail `admin@clusev.local`** (statt einer aus dem
  Hostnamen abgeleiteten Adresse, die man hätte raten müssen). Zusammen mit dem Standard-Passwort
  `clusev` ist der Erst-Login auf jeder frischen Installation vorhersehbar (`admin@clusev.local` /
  `clusev`, Zwangswechsel beim ersten Login). `--email` / `CLUSEV_ADMIN_EMAIL` überschreiben weiter;
  später unter Einstellungen → Profil änderbar. Installer-Prompt, README und Banner aktualisiert.

### Behoben
- **Command-Palette öffnete sich nach dem ersten Login von selbst und ließ sich nicht schließen**
  (nur im Prod-Build). Ursache: `app.js` baute `window.Echo` auf Top-Level; auf Auth-Seiten fehlt die
  Laufzeit-Reverb-Config (`window.__clusev`) und der Prod-Build bäckt absichtlich keine
  `VITE_REVERB_*`-Fallbacks ein, also warf der Echo-Konstruktor — und brach das Skript **vor** der
  `alpine:init`-Registrierung ab. Dadurch waren `cmdk`/`modalTrigger`/`dualChart` nicht registriert,
  Alpine konnte `x-data="cmdk(…)"` nicht auflösen, entfernte aber `x-cloak`, und das Palette-Overlay
  fiel auf seine sichtbare Default-Darstellung zurück (undismissbar). Echo wird jetzt nur bei
  vorhandener Config konstruiert, in `try/catch` gekapselt (ein Realtime-Fehler legt die UI nie lahm)
  und bei `livewire:navigated` nachgezogen (Realtime auch auf dem per-SPA erreichten Dashboard). Im
  Dev mit Vite trat es nie auf, da Vite die `VITE_REVERB_*` aus der Dev-`.env` einbäckt.

## [0.9.5] - 2026-06-17

### Geändert
- **Erst-Admin nutzt jetzt das Standard-Passwort `clusev`** (statt eines Einmal-Zufallspassworts) —
  vorhersehbarer Login. **`must_change_password` erzwingt den Wechsel beim ersten Login**, und
  Banner/MOTD/README weisen ausdrücklich darauf hin, es **sofort** zu ändern (ein bekanntes Default
  ist nur durch den erzwungenen Wechsel vertretbar). Idempotenz unverändert (kein Admin bei Re-Run).
- **Host-MOTD deutlich aufgewertet.** Statt zwei Zeilen jetzt eine orange CLUSEV-Wortmarke, die
  Version, ein **Live-Stack-Status** („X/Y Dienste aktiv", grün/rot — bei jedem Login frisch aus
  `docker compose ps`), die Dashboard-URL, eine Login-Hinweiszeile (woher die Zugangsdaten kommen —
  ohne Secret) und die Verwaltungs-Befehle. `install.sh` setzt URL, Version und den Compose-Pfad ein;
  fällt bei fehlendem Docker robust auf „Status unbekannt" zurück (bricht nie einen Login ab).

### Behoben
- **README-Voraussetzungen:** `git` ist auf minimalen Debian/Ubuntu-Images nicht vorinstalliert, der
  `git clone`-Schritt scheiterte dort vor dem Installer. Der Install-Block installiert `git` jetzt
  explizit, und „Voraussetzungen" nennt es (ein Full-VM-Installtest hat das aufgedeckt).

## [0.9.4] - 2026-06-17

### Hinzugefügt
- **„DNS prüfen & Zertifikat anfordern" im Dashboard (System → Domain & TLS).** Wurde eine Domain
  konfiguriert, deren DNS noch nicht auf den Server zeigte, lässt sich das Let's-Encrypt-Zertifikat
  jetzt per Knopf anstoßen, sobald das DNS stimmt: eine DNS-Vorabprüfung (zeigt die Domain hierher?)
  und — nur ohne klare Fehlanpassung — ein interner Handshake, der Caddys On-Demand-Ausstellung
  auslöst, plus ein persistierter Status (aktiv / DNS zeigt woanders / ausstehend). Sichtbar nur im
  eingebauten-TLS-Modus mit aktiver Domain (im Externer-Proxy-Modus ein Hinweis, bei Bare-IP nichts).
  Per-Nutzer gedrosselt (5/10 Min, auto-ablaufend — sperrt die Control-Plane nie aus) + auditiert; der
  Handshake ist fest auf den `caddy`-Dienst gepinnt (kein SSRF). Hinweis: das automatische On-Demand
  greift weiterhin von selbst beim ersten HTTPS-Aufruf — der Knopf gibt nur explizite Kontrolle +
  Statusrückmeldung.

## [0.9.3] - 2026-06-17

### Behoben
- **Modal-Trigger zuverlässig + mit Feedback.** Modale gingen direkt nach einer Navigation teils
  erst beim 2.–3. Klick auf (Konsole: „Could not find Livewire component in DOM tree"), und der
  Button gab kein Signal. Ursache: der globale Modal-Manager wurde bei jedem `wire:navigate` ab-
  und neu aufgebaut, sodass sein `openModal`-Listener kurz fehlte. Er bleibt jetzt über `@persist`
  für die ganze Sitzung bestehen — Modale öffnen beim ersten Klick. Zusätzlich zeigt **jeder**
  Modal-Trigger (in allen Views) ab Klick sofort einen Spinner und blendet einen Fehler-Toast ein,
  falls nach kurzer Zeit nichts aufgeht (neues `<x-modal-trigger>`-Element; der Toaster kennt jetzt
  eine rote Fehler-Variante). So kann ein Klick nie tot wirken und ein echtes Fehlschlagen ist nie
  stumm.

### Sicherheit
- **Passwort-Reset-Timing weiter abgeflacht.** Der Dummy-Verifikationspfad für unbekannte Konten
  spiegelt jetzt exakt die Kosten der echten Recovery-Code-Prüfung (gesperrtes `SELECT` in einer
  Transaktion statt eines schwereren bcrypt-Vergleichs), und Transportfehler beim Reset-Link werden
  geloggt statt stillschweigend verschluckt.
- **Brute-Force-/Rate-Limiting-Härtung (Audit-Nachgang).** Alle neuen Limits laufen automatisch ab
  und sperren die Control-Plane nie dauerhaft (Bare-IP-Recovery + `clusev:reset-admin` bleiben offen):
  - **Login** zusätzlich pro IP (20/Min) und pro Konto (30/15 Min) begrenzt — ein verteilter
    Mehr-IP-Angriff auf das eine Admin-Konto wird gedeckelt statt linear zu skalieren; und der
    Login-Zweig für unbekannte E-Mails führt jetzt denselben bcrypt aus (Konstantzeit) und schließt
    so das Konten-Enumerations-Timing-Orakel.
  - **TOTP-Replay-Schutz:** jeder 6-stellige Code ist genau einmal gültig (zuletzt akzeptierter
    Zeit-Schritt wird persistiert), ein abgefangener Code lässt sich nicht im 30-Sek-Fenster
    wiederverwenden.
  - **2FA-Challenge & 2FA-Beweis-Reset** zusätzlich pro Konto begrenzt (IP-unabhängiger Backstop),
    sodass die TOTP-/Backup-Code-Brute-Force-Fläche auch verteilt gedeckelt ist.
  - **Globaler `/livewire/update`-Throttle** (180/Min pro Identität) als Flut-Deckel über den
    Per-Aktion-Limitern.
  - **Re-Auth gedrosselt** (Passwortänderung/-Profil, 5/Min pro Nutzer) gegen Passwort-Brute-Force
    über eine gekaperte Session; **SMTP-Testversand** auf 3/10 Min pro Nutzer begrenzt (kein
    Spam-Relay).
  - **DoS-Schutz:** SSH bekommt einen kurzen, separaten Connect-Timeout (~5 s), damit ein toter/
    langsamer Host einen Worker nicht über mehrere sequentielle Reads × 15 s blockiert; die
    Profil-Passwort-Policy auf 12 Zeichen + Groß/Klein + Ziffern angehoben (war 10).

## [0.9.2] - 2026-06-15

### Hinzugefügt
- **Ein-Befehl-Installer (`sudo ./install.sh`).** Bootstrappt einen frischen Debian/Ubuntu-Host
  vollständig: installiert Docker aus dem offiziellen apt-Repo, legt einen dedizierten
  `clusev`-Login-Benutzer (Zufallspasswort, `docker`-Gruppe) an, prüft per DNS-Abgleich, ob die
  angegebene Domain auf diesen Host zeigt (sonst Warnung + Wahl „Domain trotzdem nehmen — HTTP bis
  DNS stimmt" oder „auf der IP bleiben"), baut/startet den Stack, migriert und legt den ersten Admin
  an. Das Abschluss-Banner zeigt ausschließlich Dashboard-URL, Admin-Login und — bei Erstanlage — den
  `clusev`-Host-Benutzer; Passwörter erscheinen nur dieses eine Mal. Idempotent (Re-Run baut/migriert
  neu, Secrets bleiben erhalten).
- **Themed Host-MOTD.** Bei jeder Host-Anmeldung erscheint ein Login-Banner in der
  Produkt-Signalfarbe mit der Dashboard-Adresse.

### Geändert
- **Professionelles README** — Produktbeschreibung, Voraussetzungen, Installation (`git clone` +
  `sudo ./install.sh`), Domain/TLS (inkl. externem Reverse-Proxy), Updates, Konto-Wiederherstellung
  und AGPL-Lizenz; ohne interne Entwicklungs-Hinweise.

### Sicherheit
- **Passwort-Reset in konstanter Zeit.** Unbekannte E-Mail-Adressen durchlaufen jetzt denselben
  Verifikationspfad (Dummy-Hash- bzw. Code-Prüfung) wie existierende Konten — die Antwortzeit verrät
  nicht mehr, ob ein Konto existiert (Account-Enumeration über ein Timing-Orakel ausgeschlossen).

## [0.9.1] - 2026-06-15

### Geändert
- **Passwort-vergessen ist jetzt SMTP-bewusst.** Mit konfiguriertem SMTP ist der Standard ein
  **E-Mail-Reset-Link (15 Minuten gültig)**; ein Zweit-Schalter „Kein E-Mail-Zugang? Mit 2FA-Code
  zurücksetzen" blendet den Inline-2FA-Reset ein. Ohne SMTP nur der 2FA-Besitznachweis-Reset. Der
  `clusev:reset-admin`-Hinweis ist von der öffentlichen Seite verschwunden und steht jetzt in
  Einstellungen → Sicherheit (eingeloggt) + README.
- **System-Seite neu komponiert** statt nur schmaler: 2-Spalten-Layout (links Konfiguration —
  Domain + TLS-Modus, rechts Status & Wiederherstellung), kritische Hinweise (Neustart, Bare-IP) als
  Banner über dem Raster.
- **Firewall/fail2ban-Bereich:** eine einzeln installierte Box füllt jetzt die volle Breite mit
  interner 2-Spalten-Aufteilung (kein leerer Raum mehr); zwei Boxen weiterhin nebeneinander.

### Sicherheit
- **Backup-Codes nur einmal sichtbar — wasserdicht.** Codes werden ausschließlich direkt nach der
  Erzeugung (erster Faktor oder „Neu erzeugen") angezeigt; „Backup-Codes verwalten" zeigt sie NICHT
  mehr an, sondern bietet nur „Neu erzeugen". Der Reveal läuft über einen transienten Kanal (nicht über
  eine persistierte Livewire-Property), sodass ein abgefangener/replayter Snapshot sie nicht erneut
  rendert; das Reveal-Flag hat eine 10-Minuten-TTL, und der Download-Endpunkt verlangt eine frische,
  einmalige Berechtigung.
- **Bestätigungs-Dialoge signiert & einmalig.** Destruktive Aktionen (Konto/Session/2FA/Hardening/…)
  laufen über server-ausgestellte, einmal gültige, signierte Tokens statt client-manipulierbarer
  Event-/Audit-Felder — Bestätigungs-Bypass und Audit-Fälschung sind ausgeschlossen; per-Aktion
  server-seitig auf das Ziel beschränkt.

### Behoben
- **fail2ban „Gesperrte IPs"-Modal öffnet sofort** — Skeleton + Lazy-Load statt blockierendem
  SSH-Read beim Öffnen.
- **Security-Keys-Untertitel** korrigiert: ein Schlüssel kann alleiniger Faktor sein, mit TOTP oder
  beides — nicht mehr „nur als zweiter Faktor".
- **System-Seite:** TLS-Modus-Schalter bricht nicht mehr um (kürzeres Label, Details in der
  Beschreibung); Domain-Feld zeigt nur noch einen Platzhalter (kein vorausgefüllter Beispielwert).
- **Einstellungen mobil:** die Tab-Leiste ist breiter als der Schirm geworden — jetzt horizontal
  scrollbar und sauber abgeschnitten (auf Desktop weiterhin vertikal).

## [0.9.0] - 2026-06-14

### Hinzugefügt
- **Mehrere Benutzer-Accounts.** Einstellungen → Benutzer: weitere (gleichberechtigte) Admin-Konten
  anlegen, auflisten, entfernen. Neuanlage erzeugt ein Einmal-Passwort (einmalig angezeigt) +
  erzwungenen Passwortwechsel beim ersten Login. Das Audit-Log hält fest, wer was getan hat.
- **Sitzungsverwaltung.** Einstellungen → Sitzungen: aktive Sitzungen ansehen (Gerät, IP, zuletzt
  aktiv), „andere Geräte abmelden" (eigene) und „alle abmelden (global)"; je Benutzer „überall
  abmelden". Jede Abmeldung rotiert den remember-Token.
- **Automatischer Neustart.** „Jetzt neu starten" in System schreibt eine Anforderung, die ein
  Host-Watcher (systemd) ausführt — kein Terminal-Befehl, kein Docker-Socket im Container.
- **SMTP-Konfiguration.** Einstellungen → E-Mail: SMTP-Server hinterlegen (Passwort verschlüsselt),
  Testmail senden. Damit funktionieren Passwort-vergessen-Mails wirklich.
- **Audit-Log-Aufbewahrung.** Konfigurierbare Aufbewahrungsdauer + täglicher `clusev:prune-audit`.
- **Datei-Manager zeigt Bilder an.** Öffnet man eine Bilddatei (png/jpg/gif/webp/svg/…), wird sie
  als Vorschau angezeigt statt im Texteditor (SFTP→base64, max. 5 MB, Speichern für Bilder aus).
- **fail2ban: gesperrte IPs im Modal.** Die lange Jail-/Bann-Liste wandert aus dem Panel in ein
  scrollbares Modal ("Gesperrte IPs ansehen") — mit Entsperren je IP.
- **Login & Passwortfelder: Anzeigen/Verbergen-Umschalter** (Auge-Icon) in Login, Passwort ändern,
  Reset und Passwort-vergessen.

### Geändert
- **Sitzungen liegen jetzt in der Datenbank** (statt Redis), damit sie auflist- und widerrufbar sind.
  **Einmaliger Effekt:** beim Update werden alle bestehenden Sitzungen ungültig — einmal neu anmelden.
- **Einstellungen in eigene Tabs** (Profil · Sicherheit · Benutzer · Sitzungen · E-Mail), jeweils als
  eigene Komponente.
- **System-Seite aufgeräumt:** Inhalt auf eine angenehme Lesebreite begrenzt (nicht mehr
  randlos), echtes Speichern-Icon, „Caddy" in der Oberfläche durch neutrale Begriffe ersetzt,
  plus Hinweis auf die SSH-Notfall-Wiederherstellung (`clusev:reset-admin`).
- **Eingabefelder:** E-Mail-Felder nutzen jetzt `type="text"` — die Validierung (inkl. Format)
  passiert ausschließlich serverseitig in der Livewire-Klasse (einheitliche, lokalisierte Meldungen).
- **Backup-Codes-Modal:** Buttons sauber ausgerichtet; der Abschluss-Button heißt jetzt „Schließen"
  (er schließt nur). Alle Modal-Textfelder sind nicht mehr in der Größe ziehbar.

## [0.8.0] - 2026-06-14

### Hinzugefügt
- **TLS über externen Reverse-Proxy (z. B. Zoraxy).** Neuer TLS-Modus in System → Domain & TLS:
  „Externer Reverse-Proxy" statt „Caddy (Let's Encrypt)". Im externen Modus holt Caddy **kein**
  Zertifikat mehr (`/_caddy/ask` verweigert) und das Panel erzwingt **keinen** eigenen
  HTTPS-Redirect — ein Proxy davor terminiert TLS und liefert an den HTTP-Port. Die Host-Prüfung
  und der Bare-IP-Recovery-Pfad bleiben in beiden Modi aktiv; die Umstellung gilt nach Stack-Neustart.
  Caddy vertraut dem `X-Forwarded-Proto` des Proxys über `TRUSTED_PROXY_CIDR` (Default `127.0.0.1/32`
  = nichts Externes); Hinweis in der UI, den HTTP-Port per Firewall nur dem Proxy zugänglich zu machen.

### Behoben (Sicherheit)
- **Onboarding-Gate auch auf Livewire-Updates.** `EnsureSecurityOnboarded` ist jetzt persistente
  Livewire-Middleware: das Gate greift auch bei `/livewire/update`-Aufrufen, nicht nur beim
  Seiten-GET — eine während des Onboardings gemountete Komponente kann keine destruktiven Aktionen
  mehr ausführen, nachdem das Konto mitten in der Sitzung auf „Passwort ändern erforderlich"
  zurückgesetzt wurde.

## [0.7.0] - 2026-06-14

### Hinzugefügt
- **Optionale, pluggbare 2FA (TOTP und/oder Security-Key — oder aus).** 2FA ist nicht mehr
  erzwungen: Der Betreiber entscheidet selbst, ob er sein Konto absichert, und wählt TOTP, einen
  Security-Key (YubiKey/WebAuthn) oder beides — oder lässt 2FA ganz aus. Empfohlen, nicht Pflicht.
  - **Onboarding erzwingt nur noch die Passwort-Rotation** (kein 2FA-Zwang mehr). Nach dem
    Passwortwechsel landet man direkt im Dashboard; 2FA wird in den Einstellungen angeboten
    (Hinweis „empfohlen").
  - **Ein Security-Key kann der einzige/erste Faktor sein** (vorher war TOTP Voraussetzung).
    TOTP und Keys werden in Einstellungen → Sicherheit unabhängig hinzugefügt/entfernt.
  - **Login-Challenge passt sich dem Faktor an:** TOTP-Nutzer sehen das Code-Feld, reine
    Key-Nutzer führen mit dem Security-Key (Backup-Code als Rückfall) — kein irreführendes
    6-stelliges Feld mehr.
  - **Backup-Codes als Modal** statt eigener Seite: werden beim ersten Faktor erzeugt und einmal
    gezeigt, später jederzeit über „Backup-Codes verwalten" wieder einsehbar / neu erzeugbar /
    herunterladbar. Die alte `/two-factor/recovery-codes`-Seite + Route entfällt (Download bleibt).
  - **Backup-Codes folgen den Faktoren:** beim Entfernen des letzten Faktors werden sie geleert,
    bei verbleibendem Faktor behalten.
  - **Passwort-vergessen ist key-only-sicher:** ein reiner Key-Nutzer setzt per Backup-Code zurück;
    ohne 2FA per E-Mail-Link (falls SMTP) oder `clusev:reset-admin`.
- **SSH-Key automatisch erstellen & Passwort-Login sicher deaktivieren.** Statt der bisherigen
  Sackgasse („Key zuerst hinterlegen") erzeugt das Panel auf Knopfdruck einen ed25519-Key,
  installiert ihn, **prüft den Key-Login**, stellt die eigene gespeicherte Verbindung darauf um —
  und schaltet Passwort-Login **erst danach** ab. Scheitert die Prüfung, bleibt Passwort-Login aktiv
  (kein Aussperren, auch nicht der Control-Plane). Der Private-Key wird einmalig angezeigt
  (Download) und verschlüsselt im Vault gehalten.
- **Schnellsuche (Strg/⌘ K) findet Server:** Tippen von Name oder IP springt direkt zur
  Server-Detailseite.
- **Journal ist live:** `journalctl` wird per Cursor inkrementell nachgeladen (alle 5 s, lückenlos,
  ohne Duplikate) statt eines einmaligen Schnappschusses; Anzeige auf 200 Zeilen begrenzt und
  scrollbar (fixe Höhe), folgt automatisch dem neuesten Eintrag.

### Geändert
- **Metriken-Broadcast-Gate:** `securityOnboarded()` heißt jetzt „Passwort rotiert" (nicht mehr
  „+ 2FA"), da 2FA optional ist — Metriken streamen an eine passwort-rotierte Session.

### Behoben
- **Alleinstehende Firewall-/fail2ban-Box** dehnte sich über die volle Breite; ist jetzt auf eine
  sinnvolle Kartenbreite begrenzt, wenn nur ein Tool installiert ist.
- **Einstellungen-Tab in der URL** (`?tab=…`): Reload/Deep-Link bleibt auf dem gewählten Tab statt
  auf „Profil" zurückzuspringen.

## [0.6.1] - 2026-06-14

### Behoben
- **Sidebar-Server-Switcher driftete von der Detailseite ab.** Es gab zwei „aktueller
  Server"-Begriffe: den Switcher (Session) und die `/servers/{uuid}`-Detailseite. Jetzt setzt der
  Aufruf einer Detailseite den Server aktiv (Sidebar spiegelt immer die Seite), und ein Wechsel im
  Switcher auf einer Detailseite navigiert zum gewählten Server statt die alte Seite neu zu laden.
  Geschrieben wird nur bei bewusster Navigation (race-frei). *Bekannte Rest-Kante:* nach
  Back/Forward (wire:navigate-Cache, kein erneutes `mount()`) zeigt die gecachte Seite den richtigen
  Server, aber die Session kann kurz auf den vorigen zeigen — löst sich bei der nächsten bewussten
  Navigation; inhärent am Single-Session-Modell (auch Multi-Tab).

## [0.6.0] - 2026-06-14

### Hinzugefügt
- **WebAuthn / Security-Keys (YubiKey) als optionaler 2. Login-Faktor.** Per
  `web-auth/webauthn-lib`. TOTP + Backup-Codes bleiben der erforderliche 2FA und die einzige
  Wiederherstellung; ein Security-Key ist eine zusätzliche Login-Option.
  - **Gated auf Domain + HTTPS:** Die rpId ist die aktive Domain — auf Bare-IP/HTTP ist die
    Funktion ausgeblendet (mit Hinweis), da eine IP keine gültige rpId ist.
  - **Einstellungen → Sicherheit:** Security-Keys hinzufügen (benannt), auflisten, entfernen
    (mit Bestätigung). Beim Login-2FA-Schritt „Mit Security-Key anmelden" neben TOTP/Backup-Code.
  - 2FA deaktivieren (UI **und** `clusev:reset-admin --disable-2fa`) entfernt jetzt auch
    Security-Keys + Backup-Codes, damit ein Reset den zweiten Faktor vollständig zurücksetzt.

### Sicherheit
- rpId strikt = aktive Domain (nie eine IP). Challenges serverseitig + einmalig.
- Signatur-Zähler-Prüfung mit Rollback-/Klon-Erkennung; Zähler 0 nur toleriert, wenn der
  gespeicherte Zähler ebenfalls 0 ist (Plattform-Authenticator/Passkeys).
- Registrierung erfordert eine aktive 2FA-Anmeldung; Label wird vor der Browser-Ceremony
  validiert (keine verwaisten Credentials). Alle Aktionen im Audit-Log.

> Hinweis: Der echte End-to-End-Test (YubiKey registrieren + anmelden) ist nur unter einer Domain
> mit HTTPS möglich; die Ceremony-Logik ist auf Options-/Speicher-/Gating-Ebene getestet.

## [0.5.0] - 2026-06-14

### Hinzugefügt
- **Kontowiederherstellung.**
  - **2FA-Backup-Codes:** 8 Einmal-Codes werden bei der 2FA-Einrichtung erzeugt, einmalig
    angezeigt und als `.txt` herunterladbar; in den Einstellungen (Sicherheit) neu erzeugbar.
    Beim Login-2FA-Schritt als Alternative zum Authenticator-Code verwendbar.
  - **„Passwort vergessen":** Reset über einen 2FA-Code oder Backup-Code (ohne E-Mail nötig).
    Zusätzlich ein E-Mail-Reset-Link, sobald ein echter Mailer konfiguriert ist
    (`mail.default` ≠ `log`/`array`).
  - **CLI-Notfall:** `php artisan clusev:reset-admin` setzt das Admin-Passwort (und optional 2FA
    + Backup-Codes) per Server-Shell zurück — der Last-Resort bei kompletter Aussperrung.

### Sicherheit
- Passwort-Resets (Formular, E-Mail-Link, CLI) rotieren das `remember_token` — eine gestohlene
  „Angemeldet bleiben"-Cookie überlebt einen Reset nicht.
- Backup-Codes werden atomar (Row-Lock) verbraucht (kein Replay).
- Der Notify-Toaster liegt jetzt auch auf den Auth-Seiten; alle Reset-/Backup-Aktionen werden im
  Audit-Log erfasst.

## [0.4.6] - 2026-06-14

### Behoben
- **Validierungsmeldungen waren nur Englisch.** Es gab keine `lang/{de,en}/validation.php`, also
  fiel Laravel bei jedem Eingabefeld-Fehler auf seine eingebauten englischen Meldungen zurück —
  unabhängig von der gewählten Sprache. Vollständige `validation.php` in Deutsch (Quelle) und
  Englisch ergänzt, plus lokalisierte Attribut-Namen (z. B. „SSH-Port", „E-Mail-Adresse"). Leeres
  Login-Formular zeigt jetzt z. B. „E-Mail-Adresse muss ausgefüllt werden.".

## [0.4.5] - 2026-06-14

### Hinzugefügt
- **Favicons + PWA-Manifest.** Vollständiger Icon-Satz aus dem App-Brand-Mark (oranges
  Server-Glyph auf Dunkel): `favicon.svg` (skalierbar), `favicon.ico` (16/32/48),
  `apple-touch-icon.png`, `icon-192/512.png` und `site.webmanifest`. In beiden Layouts via
  gemeinsames `partials/head-icons` eingebunden (incl. `theme-color`); nginx liefert das
  Manifest als `application/manifest+json`.
- **Eigene Fehlerseiten.** Gebrandete, zweisprachige Seiten für 403/404/419/429/500/503 auf
  einem eigenständigen Fehler-Layout (Design-Tokens, Brand-Mark) — ersetzen Laravels
  Standard-Fehlerseiten. Es werden nie rohe Status-Texte oder Stacktraces gezeigt; in Prod ist
  `APP_DEBUG` fest auf `false` gepinnt.

### Behoben
- **Roher Fehlertext beim Datei-Download.** Der Download-Fehler zeigte die unverpackte
  Exception-Meldung; jetzt eine lokalisierte Meldung (`files.download_failed`), konsistent mit
  Upload/Löschen.
- **Fehlerseiten-Sprache.** Früh geworfene Fehler (CSRF 419, gematchte Aborts) berücksichtigen
  jetzt die Nutzersprache (locale-Auflösung im Exception-Hook, absturzsicher gekapselt).

## [0.4.4] - 2026-06-14

### Behoben
- **Zoom beim Fokus auf Eingabefelder (Handy/Tablet).** iOS Safari zoomt die Seite hinein, wenn
  ein fokussiertes Eingabefeld eine Schriftgröße < 16px hat — durch das dichte Design (`text-xs`/
  `text-sm` auf Inputs) trat das bei jedem Feld auf. Eine zentrale Regel (`@media (pointer:
  coarse)`) erzwingt jetzt 16px für `input`/`textarea`/`select` auf Touch-Geräten (Handy + Tablet
  jeder Größe, inkl. iPad Pro 1024/1366px), während das Desktop-Layout (Maus) unverändert klein
  bleibt. Per Touch-Emulation über alle Eingabefelder verifiziert.

## [0.4.3] - 2026-06-14

### Behoben
- **Mobile Overflow durch lange URLs und IPv6-Bans.** Responsive-Audit (R7) über alle Views bei
  375/768px: mit den aktuellen Daten lief nichts über, aber lange Werte konnten Zeilen über den
  Viewport schieben. Behoben: die Panel-Domain (`break-all` am Link, `min-w-0` an der
  Zugriffsadresse) im System-Bereich und gebannte IPv6-Adressen in der fail2ban-Liste
  (`min-w-0 break-all`). Per injizierter 253-Zeichen-Domain und langer IPv6 bei 375px verifiziert.

## [0.4.2] - 2026-06-14

### Hinzugefügt
- **Server-Status „Initialisierung".** Neu angelegte Server starten in einem cyanfarbenen
  `pending`-Zustand (nicht mehr sofort rot/Offline), bis der erste Kontakt sie auf online/offline
  setzt.
- **SSH-Verbindungstest beim Anlegen.** Beim Server-Anlegen wird die SSH-Anmeldung in der
  Anlage-Transaktion geprüft (Login + Befehls-Marker beweisen, dass Befehle ausführbar sind);
  schlägt sie fehl, wird nichts gespeichert und der SSH-Grund im Formular angezeigt.
- **Hinweis „nur per SSH-Key".** Die SSH-Passwort-Login-Zeile der Härtungs-Checkliste weist
  darauf hin, dass der Zugang nach dem Deaktivieren nur noch per SSH-Key möglich ist.

### Geändert
- **Einheitliche Buttons.** Alle Buttons haben durchgehend Rahmen + Hintergrund; Löschen-Aktionen
  nutzen die neue `danger-soft`-Variante (dezent rot getönt, dauerhaft — nicht erst beim Hover).
  Die rahmenlosen `ghost`/`ghost-danger`-Varianten sind entfallen.
- **Firewall- und fail2ban-Boxen** erscheinen nur noch, wenn das jeweilige Tool installiert ist
  (oder die Statusabfrage fehlschlug), und liegen im responsiven 2-Spalten-Grid (eine Box → volle
  Breite). Die redundante „nicht installiert"-Box entfällt — installiert wird über die
  Härtungs-Checkliste.
- **Automatische Updates** gelten als Betreiber-Präferenz, nicht als Sicherheitsanforderung: der
  Zustand wird neutral als aktiv/inaktiv angezeigt und „aus" nie mehr als unsicher (OFFEN) markiert.

### Behoben
- **fail2ban/UFW fälschlich „nicht installiert".** Die Härtungs-Checkliste erkennt ein Tool jetzt
  auch dann als installiert, wenn nur die Binary auf dem PATH liegt oder der Dienst läuft (vorher
  nur `dpkg -l … ^ii`) — konsistent mit den Status-Panels. Behebt den Widerspruch „Checkliste:
  nicht installiert" vs. „Panel: aktiv, N gesperrt".

## [0.4.1] - 2026-06-14

### Behoben
- **Verstümmelter Header auf jeder eingeloggten Seite.** Ein Kommentar im `@php`-Block der
  topbar enthielt die literalen Tokens `@php`/`@endphp`; Blades Raw-Block-Matcher nahm das
  `@endphp` im Kommentar als Block-Ende und gab den Rest (inkl. `$title ??= …`) als Klartext im
  `<header>` aus — der Seitentitel blieb zudem leer. Kommentar bereinigt; neue Regel **R17**
  (keine Direktiv-Tokens in Blade-Kommentaren; gerenderte Ausgabe prüfen, nicht nur HTTP 200).

### Sicherheit
- **Realtime-Kanäle sind jetzt privat.** Der Live-Metrik-Broadcast (`MetricsTicked`) läuft über
  einen `PrivateChannel`, autorisiert über `/broadcasting/auth` (im `web`-Group → PanelScheme +
  Session). Damit kann niemand mehr allein mit dem (öffentlich gebündelten) App-Key Infrastruktur-
  Metriken abonnieren — auch nicht über einen alten Caddy-Host nach einem Domain-Wechsel. Neue
  Konvention (CLAUDE.md §3 / `routes/channels.php`): **jeder** Broadcast-Kanal ist privat.

## [0.4.0] - 2026-06-14

Die Panel-Domain ist wieder aus dem Dashboard änderbar — diesmal sicher: gespeichert wird in der Datenbank, angewendet beim Neustart, das Zertifikat holt Caddy automatisch.

### Hinzugefügt
- **Domain aus dem Dashboard ändern.** Auf der System-Seite lässt sich die Panel-Domain
  eintragen oder leeren. Gespeichert wird als Einstellung (`panel_domain`, übersteuert die
  Installations-Domain `APP_DOMAIN`); nach einem Stack-Neustart leiten sich URL, der
  WebSocket-Endpunkt (Reverb), das TLS-Zertifikat und die Cookie-Sicherheit automatisch davon
  ab. Kein Terminal-Editieren mehr — nur „speichern und neu starten".
- **Automatisches TLS per On-Demand (Caddy).** Caddy stellt das Zertifikat beim ersten
  HTTPS-Zugriff aus und fragt vorher die App (`/_caddy/ask`), ob für genau diese Domain
  ausgestellt werden darf — so kann keine fremde, auf die IP gezeigte Domain Zertifikate
  erzwingen. Voraussetzung bleibt: die Domain zeigt per DNS auf den Server.

### Geändert
- Der Reverb-Client (Browser) bezieht seinen Endpunkt jetzt zur Laufzeit aus der aktuellen
  Anfrage und läuft über dieselbe Adresse wie das Panel (Proxy/Reverse-Tunnel `/app/*`,
  `/apps/*` — Caddy in Produktion, nginx in Dev). So folgt Realtime jeder Domain-Änderung
  ohne Neu-Bauen der Assets und ohne separat geöffneten Port.
- Server-seitiges Event-Publishing läuft intern direkt zum `reverb`-Dienst, ist damit
  unabhängig von der öffentlichen Domain/dem Zertifikat.

### Sicherheit
- **Kein Aussperren.** Sicherer-Cookie-Schalter und HTTPS-Redirect richten sich nach der
  tatsächlichen Anfrage (hinter Caddy via `X-Forwarded-*`, das nur in Produktion vertraut
  wird). Der Zugriff per **IP über HTTP bleibt immer als Rückfallweg erreichbar**, falls
  eine neue Domain (DNS/Zertifikat) noch nicht bereit ist.
- **Nur die aktive Domain liefert das Panel aus.** Bei gesetzter Domain wird das Panel per
  Hostname ausschließlich über genau diese Domain (HTTPS) bedient; eine alte/fremde, auf den
  Server gezeigte Domain wird abgewiesen (404) — auch wenn Caddy ihr Zertifikat noch hält.

## [0.3.0] - 2026-06-13

Zweisprachige Oberfläche: die gesamte UI gibt es jetzt auf Deutsch und Englisch, frei umschaltbar pro Konto.

### Hinzugefügt
- **Sprache wählbar (Deutsch / Englisch).** Ein DE/EN-Umschalter (oben rechts, sowie auf der
  Login-Seite) stellt die Sprache um; die Wahl wird pro Konto gespeichert und gilt sofort für
  die gesamte Oberfläche. Ohne Wahl greift die Standardsprache (Deutsch).
- Übersetzungen liegen strukturiert unter `lang/{de,en}/<gruppe>.php` — eine Gruppe je Feature
  plus geteiltes `common`, Schlüssel in beiden Sprachen identisch.

### Geändert
- **Alle sichtbaren Texte sind lokalisiert.** Views, Komponenten und Backend-Services geben
  Text ausschließlich über `__('gruppe.schlüssel')` aus, nie mehr fest verdrahtet — neue,
  verbindliche Regel **R16**. Native Technik-Begriffe (`nginx.service`, `SSH`, `2FA`, Ports)
  bleiben unverändert.

### Entfernt
- Erläuternde „Warum …?"-Texte auf der System-Seite — die Seite zeigt nur noch Funktion und
  Status.

## [0.2.1] - 2026-06-13

Verfeinerungen an 0.2.0: vollautomatisches TLS ohne sichtbare Konfiguration und einheitliche Buttons.

### Geändert
- **TLS ist vollautomatisch.** Caddy stellt das Zertifikat aus, erneuert es und leitet
  HTTP→HTTPS um — ohne manuelle Schritte. Die System-Seite zeigt nur noch den **Status**
  (Domain, Zugriffsadresse) statt der erzeugten Caddy-Konfiguration und des
  `caddy reload`-Befehls. Die Panel-Domain ist eine **Installations-Entscheidung**
  (`APP_DOMAIN`) und bleibt damit konsistent mit URL, WebSocket (wss) und Cookie-Sicherheit;
  zum Ändern `install.sh` mit der neuen Domain erneut ausführen.
- Härtungs-Checkliste: alle Aktions-Buttons im selben Stil (einheitlich `secondary`,
  mit Rahmen) statt zustandsabhängig mal mit, mal ohne Rahmen.

### Entfernt
- Laufzeit-Override der App-URL aus der Datenbank und das dashboard-seitige Bearbeiten
  von Domain/E-Mail (konnte URL/WebSocket/Cookies nicht konsistent zur Laufzeit umstellen).
  Eine Migration entfernt die leeren Alt-Einträge; ein real konfigurierter Wert bleibt als
  Verlauf erhalten. **Upgrade-Hinweis:** eine zuvor im Dashboard gesetzte Domain bitte einmalig
  über `install.sh` setzen.

## [0.2.0] - 2026-06-13

Mehr-Betriebssystem-Verwaltung, echte Firewall- und fail2ban-Steuerung aus dem Dashboard sowie eine Tastatur-/Befehls-Bedienung.

### Hinzugefügt
- **OS-Abstraktion**: Updates, Härtung und Firewall laufen jetzt über **apt, dnf und
  zypper** (Debian/Ubuntu, RHEL/Fedora/Rocky/Alma, openSUSE). Das Betriebssystem wird
  erkannt und auf der Server-Detailseite angezeigt (Paketverwaltung/Firewall). Arch
  und Alpine werden erkannt; nicht unterstützte Funktionen werden mit deutschem
  Hinweis sauber deaktiviert statt fehlzuschlagen.
- **Firewall-Regeln (UFW)**: Regeln ansehen, hinzufügen und löschen samt
  Standard-Anzeige — mit Aussperr-Schutz (SSH-Regeln/-Port, deny ohne Port). firewalld
  wird nur angezeigt (Laufzeit aller aktiven Zonen inkl. Rich Rules); Regelverwaltung
  bleibt vorerst UFW.
- **fail2ban-Status**: Jails mit Zählern, **gebannte IPs sehen und entsperren**,
  IP manuell sperren (R5-Dialog, Aussperr-Schutz für Loopback + eigenen Zugang) und
  **Whitelist (ignoreip)** verwalten.
- **Befehls-Palette (Strg/⌘-K)** + Tastenkürzel: Schnell-Navigation (Leader „g“),
  Seitensuche („/“), Hilfe („?“).

### Geändert
- System-Updates verwenden je Distribution den passenden Paketmanager
  (Audit-Aktion `system.package_upgrade`).
- fail2ban-Tuning und -Whitelist liegen in getrennten Drop-ins, damit Änderungen sich
  nicht gegenseitig überschreiben; das frühere Einzel-Drop-in wird migriert.
- Server-Detailseite zeigt erkanntes System, Paketverwaltung und Firewall-Werkzeug.
- System-Seite erklärt jetzt, warum TLS über Caddy terminiert wird.

### Sicherheit
- Firewall: race-freies Löschen per Regel-Spezifikation (keine Regelnummern),
  Whitelisting gegen Befehls-Injection, firewalld-Lockout-Schutz (Ports vor dem Start
  in der permanenten Konfiguration).
- fail2ban: kanonischer (inet_pton) Schutz gegen das Sperren des eigenen Zugangs,
  ignoreip wird verbatim erhalten (inkl. Hostnames/Fortsetzungszeilen).

## [0.1.1] - 2026-06-13

Wartungs-Release: vollständige Code-Auditierung, Entfernung von totem Code und ein behobenes Open-Redirect. Keine funktionalen Änderungen für Nutzer.

### Behoben
- Open-Redirect im Server-Switcher: ein manipulierter `Referer` konnte die
  Weiterleitung nach dem Server-Wechsel auf eine fremde Adresse lenken. Die
  Weiterleitung wird jetzt auf einen **gleich-origin** relativen Pfad reduziert
  (fremde Schemata wie `javascript:`/`data:`, andere Hosts/Ports sowie
  protokoll-relative Tricks `//host` / `/\host` werden abgewiesen).
- SSH-Zugang: eine Passphrase wurde unabhängig vom Auth-Typ gespeichert; sie wird
  jetzt nur noch bei Schlüssel-Authentifizierung hinterlegt.

### Geändert
- Datei-Manager + Editor verwenden ein eigenes `file`-Symbol statt des
  Audit-Symbols (semantisch korrekt).
- Code-Hygiene: toten Code entfernt (ungenutzte `FirewallService`-Methoden
  `status/allow/deny`, `Server::auditEvents()`, `SshCredential::scopeActive()`,
  ungenutztes `bell`-Icon), veraltete Doc-Blöcke/Kommentare korrigiert,
  Exceptions sauber importiert, base64-Argument im Sudo-Pfad defensiv gequotet,
  Null-sichere Nutzerabfrage in den Einstellungen. Pint + Codex-Review sauber.

## [0.1.0] - 2026-06-13

Erste geschnittene Version — die komplette v1-Basis inklusive Server- und Panel-Härtung als ein Release.

### Hinzugefügt
- Dashboard mit Live-Sparklines und poll-getriebenem Dual-Chart (KPIs + Tabelle).
- Server-Index + Detailseite, Dienste (systemd: Start/Stop/Restart + Journal),
  Datei-Manager (SFTP: Up-/Download, Editor), Audit-Log.
- SSH-Schicht über phpseclib (exec + SFTP) mit Host-Key-Pinning (TOFU) und
  Multi-Server-Switcher samt Fleet-Kontext.
- Echte Metriken (CPU/RAM/Disk/Load) inkl. Absolutwerten über Reverb.
- Authentifizierung + 2FA (TOTP) mit erzwungenem Onboarding.
- R5-Bestätigungsmodals (wire-elements/modal) für destruktive Aktionen.
- System-Seite mit Domain & TLS (Caddy Auto-TLS) und Release-Kanal.
- Echte Versions-Seite, die Version, Build-Hash und dieses Änderungsprotokoll liest.
- Deploy: `install.sh` + Caddy (Auto-TLS) + Produktions-Härtung.
- Selbst-gehostete Fonts (kein CDN), Split-Brand Login/2FA.

### Geändert
- Dienst-Steuerung läuft über das hinterlegte Sudo-Passwort (`sudo -S`) statt nur
  passwortlosem sudo — Start/Stop/Restart + Journal funktionieren live.
- Lazy-Loading der Seiten (Skeletons), normalisierte Buttons + Spinner,
  Detailseiten- und Einstellungs-Redesign, Schlüsselerzeugung.
- Vollständige Regel-Auditierung (R1–R14), neue Regel R14 (lokale Fonts).

### Behoben
- Datei-Editor: Klick löste einen Alpine-Fehler aus (`@js()` kompiliert nicht in
  Komponenten-Attributen) — auf Index-Lookups umgestellt.
- Adversarial Review der SSH-Schicht (Escaping, Parser, Host-Key-Pinning).
- 500 durch `@disabled` auf `<x-btn>`; Modal über dem Backdrop; poll-getriebene
  Metriken (Chart + Gauges) statt fiktiver Fleet-Daten; 2FA-Setup-QR als `<img>`.
- fail2ban/apt-Härtung schlug fehl, weil der 12s-SSH-Timeout den Paket-Install
  abbrach — langer Timeout für apt-Aktionen.

### Sicherheit
- Server-Härtung aus dem Dashboard (Root-/Passwort-Login, fail2ban, UFW,
  Auto-Updates) mit Befehls-Vorschau, Bestätigung und Aussperr-Schutz.
- Verwaltung hinterlegter SSH-Zugänge (Name, Sperren, Löschen); der Vault
  verweigert gesperrte Zugänge. Dienst-Steuerung über hinterlegtes Sudo-Passwort.
- Panel-Selbst-Härtung: Security-Header + umgebungsbewusste CSP, 2FA-Brute-Force-
  Drossel, Session-Cookie-Härtung.
- Release-/Versionierungsmodell: echte semantische Versionen + Git-Tags, Changelog
  pro Version, Kanal `stable` (kein nutzer-seitiges `dev`).

[Unreleased]: https://github.com/clusev/clusev/compare/v0.2.1...HEAD
[0.2.1]: https://github.com/clusev/clusev/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/clusev/clusev/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/clusev/clusev/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/clusev/clusev/releases/tag/v0.1.0
