# Spec — Der Host installiert sich selbst, CluPilot übernimmt ihn nur

**Datum:** 2026-07-30
**Status:** entworfen, noch nicht umgesetzt
**Basis:** `feature/betriebsmodus` (Betriebsmodus + Bereitschaftsseite)
**Vorgänger-Kontext:** `docs/handoffs/2026-07-30-real-run-handoff.md` §3, Blöcke A–C

---

## 1. Ziel

Die Verantwortungsgrenze verschiebt sich. **CluPilot installiert nichts mehr.** Ein
Bootstrap-Skript macht aus einer nackten Maschine einen fertigen Proxmox-Host mit
Traefik, WireGuard-Beitritt und goldener Nextcloud-Vorlage. CluPilot übernimmt ihn
danach und benutzt ihn.

Der einzige dauerhafte Zugang, den die Plattform zu einem Host behält, ist der
**Proxmox-API-Token**. Kein SSH, keine root-Shell, nichts, was eine Shell braucht.

### Warum

Der Betreiber will „so wenig Fehler wie möglich". Die drei teuersten Fehlerquellen
des heutigen Entwurfs sind alle Installationsarbeit über SSH:

- **`vmbr0` vollautomatisch** — im Handoff selbst als „riskantester Code des
  Vorhabens" bezeichnet. Eine Brücke, die für einen Anbieter richtig ist, nimmt bei
  einem anderen die Maschine vom Netz. `ConfigureProxmox` bricht heute bewusst mit
  einer Anleitung ab, statt es zu versuchen.
- **Traefik als Systemdienst über SSH installieren**, samt Prüfung, dass er lauscht.
- **Die goldene Vorlage als Runbook.** Ein Dokument für einen Menschen, mit genau
  den Fallen, die der Handoff aufzählt: `user: www-data` vergessen, LVM statt einer
  letzten Partition, `qemu-guest-agent` nicht im Image.

Alle drei verschwinden als Plattformarbeit, wenn die Maschine fertig ankommt.

### Nicht Teil dieses Vorhabens

- **Teil C des Betriebsmodus-Vorhabens** (Testdaten markieren, eigene Belegserie,
  Aufräumen inkl. VM und DNS). Weiterhin beschlossen, weiterhin eigener Entwurf.
- Das Bestellen des Servers und das Starten des Rettungssystems. Das passiert im
  Kundenbereich von Hetzner oder netcup und ist von hier aus nicht erreichbar.
- Migration bestehender Hosts. Es gibt keine echten; die zwei in der Datenbank sind
  Seed-Daten aus `DatabaseSeeder` und werden entfernt, nicht überführt.

---

## 2. Ausgangsbefund — gemessen, nicht gelesen

Am 2026-07-30 auf dem Stand von `feature/betriebsmodus` nachgemessen.

| Befund | Beleg |
|---|---|
| Die Host-Kette hat **14 Schritte** | `config/provisioning.php`, `pipelines.host` |
| Sie bleibt beim siebten stehen | `ConfigureProxmox` liefert `fail`, wenn `ip link show vmbr0` nichts findet |
| **Kein Traefik-Schritt** in der Kette | Block C nie gebaut |
| Routen gehen **per SSH als root** auf den Host | `SshTraefikWriter::write()` → `connectWithKey($trafficHost, 'root', …)` |
| Und zwar **nicht einmalig** | Aufrufer: `ConfigureDnsAndTls`, `RunAcceptanceChecks`, `EndInstanceService` |
| Die Bereitschaftsseite meldet 6 blockierende Lücken | `Readiness::blocking()`, davon 2 echt |

Der vierte und fünfte Befund sind der Grund, warum „Zugang nur für die
VM-Erstellung" heute **nicht** stimmt: das Routing braucht eine dauerhafte
root-Shell auf jedem Kundenhost.

---

## 3. Entscheidungen aus dem Brainstorming (verbindlich)

| Frage | Entscheidung |
|---|---|
| Wer installiert? | **Ein Bootstrap-Skript auf der Maschine.** CluPilot nie. |
| Wie kommt eine Kundenroute auf den Host? | **Der Host holt sie sich.** Traefik-HTTP-Provider fragt einen CluPilot-Endpunkt im Tunnel ab. |
| Was hinterlässt das Skript? | **Alles, inklusive der goldenen Vorlage** auf `template_vmid 9000`. |
| Wie finden Host und Konsole zueinander? | **Einmal-Code aus dem Adminbereich**, alles Weitere im Tunnel. |
| Was sieht die Konsole währenddessen? | **Schritt für Schritt**, gemeldet vom Host. |
| Dauerhafter Zugang der Plattform | **Nur der Proxmox-Token.** |

---

## 4. Der Ablauf, wie ihn der Betreiber erlebt

1. Server beim Anbieter bestellen, **Rettungssystem starten** (Kundenbereich des
   Anbieters).
2. Im Adminbereich **Host anlegen**: Name, Rechenzentrum, öffentliche IP. Die
   Konsole zeigt **eine Zeile zum Kopieren** mit dem Einmal-Code darin.
3. Diese Zeile **einmal** im Rettungssystem einfügen.
4. Zusehen. Der Fortschritt läuft in der Konsole mit.
5. Der Host steht auf `active`, sobald er sich gemeldet und die Prüfungen bestanden
   hat.

Zwei Stellen, an denen der Betreiber etwas tut. Kein Wert wird abgetippt.

---

## 5. Kein öffentlicher Endpunkt — die Befehlszeile trägt alles

### Das Henne-Ei-Problem

Alles soll im Tunnel passieren, aber um dem Tunnel beizutreten, braucht das Skript
die WireGuard-Angaben. Ein erster Entwurf löste das mit einem öffentlichen
`POST /enrol`. **Das war unnötig**, und der Betreiber hat es zu Recht bestritten.

Die drei Werte, die das Skript vor dem Tunnel braucht — Hub-Schlüssel,
Hub-Adresse, zugeteilte Tunnel-IP — kennt die Konsole beim Anlegen des Hosts alle.
Sie kann sie **in die kopierte Befehlszeile schreiben**. Es gibt nichts abzurufen.

### Der eine Haken, und wie er aufgelöst wird

Damit der Hub den Host hereinlässt, muss er dessen **öffentlichen** Schlüssel
kennen. Erzeugte der Host ihn selbst — wie `ConfigureWireguard` es heute tut —,
müsste er ihn melden, **bevor** der Tunnel steht. Genau dafür wäre ein öffentlicher
Endpunkt nötig gewesen.

Also erzeugt **CluPilot** das Schlüsselpaar beim Anlegen des Hosts und gibt es mit.
Der Hub kennt den Peer damit von Anfang an, und es gibt keinen Rückkanal vor dem
Tunnel.

### Und danach wird getauscht

Sobald der Tunnel steht, erzeugt der Host mit `wg genkey` einen **frischen**
Schlüssel, meldet den öffentlichen Teil **durch den Tunnel** (§6), und CluPilot
tauscht den Peer aus. Der Schlüssel, der je in einer Befehlszeile und einer
Zwischenablage stand, ist danach wertlos.

Das ist der Grund, warum diese Lösung der öffentlichen Tür vorzuziehen ist: der
Preis ist ein Schlüssel mit einer Lebensdauer von Minuten, der Gewinn ist, dass die
Netzgrenze **unverändert** bleibt. Es kommt kein einziger neuer Weg von außen
hinzu.

### Der Einmal-Code

Bleibt trotzdem, aber er ist jetzt nur noch ein **Ausweis für den Rückweg**, nicht
der Schlüssel zu einer Auskunft: 32 Zeichen aus `random_bytes`, in der Datenbank
**nur als Hash**, mit `expires_at` (**24 Stunden**, großzügig — er öffnet nichts
nach außen) und `used_at`. Das Skript legt ihn den Aufrufen aus §6 und §7 bei,
damit ein Gerät im Tunnel nicht für einen fremden Host sprechen kann.

---

## 6. Der Rückweg: Token durch den Tunnel

Nachdem das Skript dem Tunnel beigetreten ist, meldet es sich über die
**Tunnel-Adresse** von CluPilot — nicht über die öffentliche.

`POST /host/register` (nur aus dem WireGuard-Subnetz erreichbar, `TRUSTED_RANGES`)
mit dem Einmal-Code als Ausweis und:

- dem Proxmox-API-Token, den das Skript auf der Maschine erzeugt hat
- dem **frisch erzeugten WireGuard-Pubkey** (§5): CluPilot tauscht den Peer aus und
  verwirft den, der in der Befehlszeile stand
- dem SSH-Host-Schlüssel-Fingerabdruck (nur zur Anzeige; die Plattform verbindet
  sich nicht mehr)
- Knotenname, `pve_version`, Plattengröße, RAM, Kerne

**Reihenfolge beim Schlüsseltausch:** erst den neuen Peer aufnehmen, dann den alten
entfernen. Andersherum schneidet sich der Host im selben Aufruf den Ast ab, auf dem
er sitzt — und die Antwort erreicht ihn nie.

CluPilot legt den Token in den Tresor (`SecretVault`, verschlüsselt mit
`SECRETS_KEY`) und schreibt `hosts.api_token_ref`. **Der Betreiber sieht den Token
nie.**

Warum durch den Tunnel: ein Proxmox-Token, der über einen öffentlichen Endpunkt
gereicht wird, liegt für die Dauer der Anfrage in fremden Protokollen und
Zwischenspeichern. Der Tunnel ist ohnehin da, wenn dieser Aufruf passiert.

---

## 7. Fortschritt: der Host erzählt, die Konsole zeigt

`POST /host/progress` (ebenfalls nur aus dem Tunnel, mit dem Einmal-Code als
Ausweis) meldet je Abschnitt einen Schlüssel und einen Zustand.

Die Abschnitte **vor** `wireguard_joined` kann der Host naturgemäß noch nicht
melden — es gibt bis dahin keinen Weg zu CluPilot. Sie werden lokal mitgeschrieben
und beim ersten erreichbaren Aufruf **nachgereicht**, mit ihren Zeitstempeln. Die
Konsole zeigt bis dahin „wartet auf den Tunnel" und, nach einer Frist, dass sie
lange wartet.

Die Abschnitte des Skripts, in dieser Reihenfolge:

| Schlüssel | Was passiert |
|---|---|
| `rescue_checked` | Virtualisierung vorhanden (`vmx`/`svm`), Platte groß genug, Netz da |
| `debian_installed` | Grundsystem geschrieben, Partition **als letzte, ohne LVM** |
| `rebooted` | Maschine ist aus dem Rettungssystem heraus neu gestartet |
| `proxmox_installed` | PVE-Pakete drauf, Kernel aktiv |
| `network_bridged` | `vmbr0` steht, Maschine antwortet noch |
| `wireguard_joined` | Handshake mit dem Hub bewiesen |
| `traefik_running` | Dienst läuft, lauscht auf 80/443, holt seine Routen ab |
| `template_built` | Vorlage auf 9000, als Template konvertiert |
| `registered` | Token übergeben, Host meldet sich fertig |

Die Konsole zeigt dieselbe Fortschrittsdarstellung wie heute. Bleibt eine Meldung
länger als eine gesetzte Frist aus, steht dort **welcher Abschnitt** offen ist —
nicht „irgendetwas ging schief".

**`network_bridged` ist der gefährliche Abschnitt** und behält deshalb die
Selbstrücknahme aus Block B des Handoffs, nur im Skript statt in der Pipeline:
Netzkonfiguration sichern, Zeitgeber auf fünf Minuten der sie zurückspielt, Brücke
aktivieren, **von außen** prüfen ob die Maschine noch antwortet, erst dann den
Zeitgeber abbestellen. Eine Maschine, die dabei vom Netz geht, kommt von selbst
zurück.

---

## 8. Das Routing dreht die Richtung um

Heute schreibt CluPilot eine YAML-Datei per SSH in Traefiks dynamisches
Verzeichnis. Künftig liefert CluPilot sie aus und Traefik holt sie ab.

- Neuer Endpunkt `GET /host/routes` (nur aus dem Tunnel), der **alle** Routen dieses
  Hosts als eine Traefik-Konfiguration ausliefert.
- Er braucht ein **dauerhaftes Host-Token**, nicht den Fortschritts-Token: der
  Fortschritts-Token gilt für die Einrichtung und ist danach wertlos, während die
  Routen für die Lebensdauer des Hosts abgerufen werden. CluPilot gibt es bei
  `POST /host/register` (§6) zurück, das Skript legt es in Traefiks statische
  Konfiguration. Es liegt wie jedes Zugangsdatum verschlüsselt im Tresor und ist
  je Host verschieden — ein entwendetes Token zeigt die Routen genau eines Hosts,
  nicht die aller.
- Traefiks statische Konfiguration bekommt einen `http`-Provider auf diese URL, mit
  Abrufintervall.
- `SshTraefikWriter` und die `RemoteShell`-Abhängigkeit der Routen-Schreiber
  entfallen. `TraefikWriter` bleibt als Schnittstelle; die Aufrufer
  (`ConfigureDnsAndTls`, `RunAcceptanceChecks`, `EndInstanceService`) ändern sich
  **nicht** — sie schreiben weiter in die Datenbank, nur liest jetzt der Host.

**Der Nebengewinn ist größer als die Ersparnis an Zugang:** heute ist ein
fehlgeschlagener Schreibvorgang verloren und niemand merkt es. Künftig holt sich
ein Host, der eine Stunde nicht erreichbar war, seinen Stand beim nächsten Abruf
von selbst wieder. Der Zustand konvergiert, statt zu driften.

**Namen müssen passen:** Der Handoff warnt in Block C ausdrücklich, dass
Entrypoint- und certResolver-Namen zu dem passen müssen, was `SshTraefikWriter::render()`
schon ausgibt — „hier zuerst nachlesen, nicht raten". Das gilt unverändert für die
statische Konfiguration, die das Skript schreibt.

---

## 8a. Das Netzbild, ausdrücklich

Vom Betreiber festgelegt, und der Grund für mehrere Entscheidungen oben.

| Auf dem Host | |
|---|---|
| **80/443** | öffentlich. Die Kunden-Clouds müssen erreichbar sein. |
| **22 (SSH)** | **nur im Tunnel.** Aus dem Netz nicht sichtbar. |
| **8006 (Proxmox)** | **nur im Tunnel.** Aus dem Netz nicht sichtbar. |
| alles übrige | zu |

| Bei CluPilot | |
|---|---|
| öffentlich | **nur, was heute schon offen ist** — Website und Portal |
| `register`, `progress`, `routes` | ausschließlich aus dem WireGuard-Subnetz |

**Es entsteht kein einziger neuer Weg von außen.** Das ist die härteste Bedingung
dieser Spec, und §11 Test 1 hält sie fest.

Der Preis, bewusst getragen: fällt die Steuerungs-VM aus, holt kein Host seine
Routentabelle. Die Kunden merken davon nichts — Traefik beantwortet Anfragen aus
seiner lokalen Tabelle weiter. Verzögert wird nur eine **Änderung** (eine frisch
nachgewiesene Kundendomain, eine Kündigung), und die holt der Host beim nächsten
Abruf **von selbst** nach. Das ist besser als heute, wo ein fehlgeschlagener
Schreibvorgang verloren ist und niemand es bemerkt.

Zur Klarstellung, weil es im Gespräch zweimal durcheinanderging: WireGuard hat
keine Verbindung, die abreißen kann — es ist zustandsloses UDP und nimmt den
Verkehr von selbst wieder auf. Was ausfallen kann, ist ein **Ende**: die
Steuerungs-VM oder der Host. Das repariert kein Protokoll, und es gilt für jede
Variante gleichermaßen.

---

## 8b. Die Subdomain gehört dem Kunden

Heute leitet `ReserveResources::uniqueSubdomain()` sie aus dem Kundennamen ab
(`Str::slug($customer->name).'-'.Str::random(5)`). Der Kunde wird nie gefragt.

**Künftig wählt er sie an der Kasse**, mit drei Vorschlägen aus seinem eigenen
Namen und der Möglichkeit, etwas anderes einzugeben. Leeres Feld → ein
unauffälliger Zufallsname.

### Warum an der Kasse und nicht später

Die Subdomain ist die **Kennung** der Instanz: `instances.subdomain`, der A-Record
in unserer Zone, der interne Hostname, das erste Zertifikat, ein Eintrag in
Nextclouds `trusted_domains`. Sie nachträglich zu ändern heißt, alles davon
gleichzeitig umzuhängen — eine Migration mit Ausfall.

**Nicht zu verwechseln mit der eigenen Domain des Kunden.** Die kommt später
**dazu** und ersetzt nichts: `ConfigureDnsAndTls` bildet
`$hostnames = array_filter([$fqdn, $customDomain])` und serviert beide. Dazunehmen
ja, umbenennen nein.

### Warum ein Hinweis danebensteht

Eine Subdomain ist **dauerhaft öffentlich** — nicht nur im DNS. Jedes
Let's-Encrypt-Zertifikat wird in die Certificate-Transparency-Protokolle
eingetragen; die sind öffentlich, durchsuchbar und werden nie gelöscht.
`mueller.clupilot.cloud` verrät damit für immer, dass ein Müller hier Kunde ist.
Einer GmbH ist das gleich, einer Arztpraxis womöglich nicht. Der Kunde soll das
wissen, bevor er wählt — deshalb der Satz neben dem Feld, und deshalb ein
neutraler Zufallsname als Vorgabe für alle, die nichts eintragen.

### Regeln

- **Sperrliste**, mindestens `www`, `mail`, `admin`, `api`, `app`, `ws`, `status`,
  `vpn` — sonst nimmt sich jemand einen Namen, den die Plattform selbst braucht.
- **Eindeutig über alle Kunden**, ohne Rücksicht auf Groß- und Kleinschreibung.
- **Reserviert beim Absenden**, nicht erst wenn die VM steht: zwei gleichzeitige
  Bestellungen dürfen nicht nach demselben Namen greifen.
- Länge und erlaubte Zeichen nach DNS-Regeln, keine führenden oder abschließenden
  Bindestriche.

---

## 9. Die Host-Kette schrumpft

| Heute | Künftig |
|---|---|
| `ValidateHostInput` | bleibt |
| `EstablishSshTrust` | **entfällt** |
| `PrepareBaseSystem` | **entfällt** |
| `ConfigureWireguard` | **entfällt** (das Skript tritt bei; die Kette prüft nur den Handshake) |
| `InstallProxmoxVe` | **entfällt** |
| `RebootIntoPveKernel` | **entfällt** |
| `ConfigureProxmox` | **entfällt** — Block B erledigt sich |
| `CreateAutomationToken` | **entfällt** (das Skript erzeugt ihn) |
| `VerifyProxmoxApi` | bleibt, wird zentral |
| `VerifyVmTemplate` | bleibt |
| `RegisterHostDns` | bleibt |
| `RegisterCapacity` | bleibt |
| `SecureHostFirewall` | **entfällt** als Schreiben, bleibt als **Prüfung** |
| `CompleteHostOnboarding` | bleibt |

Sechs Schritte statt vierzehn, und die verbleibenden **sehen nach, statt zu
verändern**. Ein Host, der die Kette nicht besteht, wird nicht repariert — er wird
neu aufgesetzt, was jetzt ein Befehl ist.

---

## 10. Was an der Bereitschaftsseite nachzuziehen ist

Das Betriebsmodus-Vorhaben hat Prüfungen gebaut, die von der alten Welt ausgehen:

- `onboarding.ssh_private_key` — **entfällt als blockierend.** Die Plattform
  braucht keinen SSH-Schlüssel mehr. Der Tresor-Eintrag bleibt (Altbestand,
  Notfallzugang), aber sein Fehlen hält nichts mehr auf.
- `provisioning.traefik_path` — **entfällt.** Es gibt kein Verzeichnis mehr, in das
  CluPilot schreibt.
- Neu: **der Routen-Endpunkt ist erreichbar** und mindestens ein Host holt ihn
  tatsächlich ab (letzter Abruf jünger als das Intervall). Ein Host, der seine
  Routen nicht mehr holt, serviert stillschweigend einen veralteten Stand — genau
  die Sorte stiller Fehler, gegen die die Seite gebaut ist.

Der Wächter-Test aus Aufgabe 12 erzwingt, dass jeder Tresor-Eintrag auf der Seite
auftaucht; Änderungen an der Registry ziehen ihn nach.

---

## 11. Tests

Ein Skript, das ein Betriebssystem installiert, lässt sich nicht in SQLite prüfen.
Die Beweislast teilt sich:

**Was die Suite prüfen kann und muss:**

1. **Es entsteht kein öffentlich erreichbarer Weg.** Ein Test hält fest, dass
   `register`, `progress` und `routes` alle drei hinter der Tunnel-Beschränkung
   liegen — der wichtigste Test dieser Spec, und der Grund, warum der Entwurf so
   aussieht.
2. Ein Einmal-Code gilt **einmal**, verfällt nach 24 Stunden und liegt **nur als
   Hash** in der Datenbank.
3. Ein Aufruf mit dem Code eines **fremden** Hosts wird abgewiesen — sonst spricht
   ein Gerät im Tunnel für eine Maschine, die ihm nicht gehört.
4. Der Proxmox-Token landet im Tresor und **nie** in einer Antwort, einem Protokoll
   oder einer Ansicht.
5. **Der Schlüsseltausch nimmt erst auf, dann weg.** Ein Test, der die Reihenfolge
   umdreht, muss zeigen, dass der Host dabei aus dem Tunnel fällt — sonst ist die
   Reihenfolge nur ein Kommentar.
6. Der Schlüssel aus der Befehlszeile ist nach dem Tausch **nicht mehr gültig**.
7. `GET /host/routes` liefert genau die Routen dieses Hosts und keine fremden.
8. Die geschrumpfte Kette hat die sechs Schritte in der festgelegten Reihenfolge.
9. Nachgereichte Fortschrittsmeldungen behalten **ihre** Zeitstempel, nicht den des
   Eingangs — sonst sieht eine zwanzigminütige Installation aus wie eine Sekunde.

**Was nur auf echter Hardware zu beweisen ist:** alles im Skript. Der Handoff sagt
dazu das Richtige — *„Jeder Schritt, der scheitert, ist ein Fund — und der Server
darf dabei kaputtgehen, Debian ist neu eingespielt in Minuten."* Der dedizierte
Testserver ist der Prüfstand, und das Skript ist so gebaut, dass ein zweiter Lauf
auf einer frisch aufgesetzten Maschine immer gleich ausgeht.

---

## 12. Reihenfolge

1. Einmal-Code (Hash, Ablauf, Verbrauch) und die Schlüsselpaar-Erzeugung beim
   Anlegen des Hosts — mit den Sicherheitstests aus §11.
2. Tunnel-Endpunkte `register` und `progress`, Token in den Tresor, Schlüsseltausch
   in der richtigen Reihenfolge.
3. Adminbereich: Host anlegen zeigt die Befehlszeile, Fortschritt läuft mit.
4. Routen-Endpunkt und Umstellung von `SshTraefikWriter` auf den Abruf.
5. Die Kette schrumpfen, Bereitschaftsprüfungen nachziehen.
6. Das Bootstrap-Skript, Abschnitt für Abschnitt, mit Rückmeldung nach jedem.
7. Auf echter Hardware durchlaufen lassen.

Schritt 1 bis 5 ist prüfbare Plattformarbeit. Schritt 6 ist das Skript. Schritt 7
ist die Abnahme, und sie ist die einzige, die zählt.

---

## 13. Risiken

- **Schlüsselmaterial in der Zwischenablage.** Der WireGuard-Schlüssel, mit dem der
  Host dem Tunnel beitritt, steht in der kopierten Befehlszeile und damit kurz in
  der Zwischenablage und im Verlauf des Rettungssystems. Entschärft dadurch, dass
  er Minuten lebt und nach dem Tausch (§5) wertlos ist — und dass das
  Rettungssystem beim ersten Neustart ohnehin verworfen wird. Wer den Tausch
  weglässt, hat diesen Punkt dauerhaft; deshalb ist er nicht optional.
- **Der Fortschritt hängt am Host.** Ein Skript, das stirbt, bevor es meldet,
  hinterlässt eine Konsole, die auf etwas wartet, das nie kommt. Deshalb je
  Abschnitt eine Frist und eine sichtbare Aussage, welcher offen ist.
- **Das Skript ist nicht wie die übrige Codebasis prüfbar.** Das ist ein echter
  Rückschritt in der Beweisbarkeit und der Preis dafür, dass die riskanteste
  Arbeit die Plattform verlässt. Er ist es wert, aber er ist nicht null.
- **Traefik holt sich Routen — auch veraltete.** Fällt CluPilot aus, servieren die
  Hosts weiter, was sie zuletzt geholt haben. Das ist die richtige Richtung, muss
  aber bewusst so sein und gehört auf die Bereitschaftsseite (§10).

---

## 14. Zwei beschlossene Vorhaben, die hier NICHT drin sind

Beide im selben Gespräch entschieden, beide bekommen einen eigenen Entwurf. Sie
stehen hier, damit die Entscheidungen nicht verlorengehen.

### Nextcloud zurücksetzen (Kundenfunktion)

Der Kunde kann seine Nextcloud auf Werkszustand zurücksetzen — wenn er etwas
zerschossen hat oder neu anfangen will. Technisch derselbe Ablauf wie eine
Erstbereitstellung, nur für einen Kunden, der schon bezahlt hat.

Beschlossen: **vorher ein Sicherungspunkt, 14 Tage aufbewahrt**, mit einer
Wiederherstellung, die der Kunde selbst auslösen kann. Nur der Kontoinhaber, nicht
jeder Platz. Subdomain zur Bestätigung abtippen statt nur klicken — „neu starten"
und „zurücksetzen" lesen sich zu ähnlich. Sperrfrist zwischen zwei Versuchen.
Bestätigung im Modal nach R23.

**Die Adresse bleibt:** dieselbe Subdomain, dasselbe Zertifikat, eine hinterlegte
eigene Domain bleibt hinterlegt. Der Kunde bekommt eine leere Nextcloud unter
seiner gewohnten Adresse, keine neue Adresse.

### Störungsmeldung an den Betreiber

**Gemessen, dass es das heute nicht gibt:** `SyncMonitoringStatus` läuft alle fünf
Minuten und schreibt den Zustand mit, legt aber **keinen Vorfall an** und schickt
**keine Mail**. In `app/Mail/` gibt es keine einzige Störungsmeldung — alle
vorhandenen Mails gehen an Kunden. Eine Kunden-VM kann also ausfallen, ohne dass
jemand es erfährt.

Beschlossen: **zwei Fehlmessungen in Folge** (der Zeitplan misst fünfminütig, das
sind die gewünschten zehn Minuten) lösen eine Störung aus. Ein einzelner Aussetzer
nicht.

**Fällt ein ganzer Host aus, kommt EINE Meldung** — „Host pve-fsn-1 antwortet
nicht, 40 Instanzen betroffen" — statt vierzig einzelner. Vierzig Mails sagen
nicht, dass es ein Host ist; eine sagt es sofort. Einzelne VMs melden sich
weiterhin einzeln.

Damit es Signal bleibt und nicht Lärm: keine Meldung während eines angekündigten
Wartungsfensters, keine für gekündigte oder abgeschaltete Instanzen, **eine Mail je
Störung** statt alle fünf Minuten eine neue, und eine Entwarnung, wenn es zurück
ist.

---

## 15. Folgepunkte

- Die zwei Seed-Hosts aus `DatabaseSeeder` entfernen. Sie stehen auf `active`,
  haben keinen Token und tragen Adressen aus RFC 5737.
- Teil C des Betriebsmodus-Vorhabens (Testdaten und Aufräumen).
- Die geparkten Befunde aus `.superpowers/sdd/…/progress.md`, insbesondere der
  zweite `source()`-Leser in `integrations.blade.php`.
