Commit Graph

13 Commits (main)

Author SHA1 Message Date
nexxo 4ad1828f27 Ein reservierter Host gehoert seinem Kunden
placeableIn() nahm jeden aktiven Host im Rechenzentrum, und hasRoomFor() sowie
largestPlaceableGb() zaehlten eine exklusiv verkaufte Maschine obendrein mit —
der Shop versprach damit Platz, der bereits vergeben war. Ohne diese Markierung
ist 'eigener Server' ein Satz im Angebot und nirgends eine Tatsache.

hosts.reserved_for_customer_id (nullable, ueberlebt den Kunden) markiert die
Maschine; Host::placeableIn() und HostCapacity zaehlen sie nur noch fuer den
eigenen Mieter oder gar nicht mehr zum allgemeinen Bestand. Auf der
Host-Detailseite kann ein Operator reservieren und wieder loesen — Loesen
laeuft ueber ein eigenes Bestaetigungsmodal (R23), das selbst nichts aendert,
sondern nur an HostDetail::releaseReservation() zurueckmeldet. Host-Liste und
Kapazitaetsseite weisen eine reservierte Maschine als solche aus, statt sie
kommentarlos verschwinden zu lassen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:06:14 +02:00
nexxo 11ba7ee34f Hostdetailseite: die Adresse steht neben den IPs und ist ein Klick zur Proxmox-Oberflaeche 2026-08-01 14:12:03 +02:00
nexxo 55b749c129 Release v1.3.94 — die Kurven zeigen wieder, wie ausgelastet der Host ist
tests / pest (push) Failing after 9m19s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details
Drei Punkte des Besitzers, und der dritte hat den eigentlichen Fehler
freigelegt.

Die Verlaufslinien sahen komisch aus, weil sie auf ihr EIGENES Minimum und
Maximum skalierten. Ein Host mit 0,2 % CPU, der zwischen 0,1 und 0,3 schwankt,
zeichnete damit einen Seismographen über die volle Höhe — und widersprach flach
der Zahl direkt daneben. Dieselbe Regel stand im Chart.js-Entwurf schon
ausformuliert ("eine automatisch skalierte Achse lässt 3 % wie eine Wand
aussehen") und ging beim Umstieg auf die Kacheln verloren.

x-ui.spark bekommt deshalb `min`/`max`. Ohne Angabe bleibt alles wie bisher —
das ist, was jeder bestehende Aufrufer übergibt. Die Prozent-Kacheln geben
0 und 100 mit, die Netz-Kacheln nur die 0, weil MiB/s keine natürliche
Obergrenze haben. Ein untätiger Host zeichnet jetzt vier ruhige Linien statt
zweier wogender und zweier flacher — vorher wirkte CPU (0,2 %) belebter als RAM
(3,6 %), also genau verkehrt herum.

Dazu: Werte außerhalb der Grenzen werden geklemmt statt aus dem Kasten
gezeichnet, die Fläche ist ein Verlauf statt einer harten Kante, und die
Linien sind mit 120×40 statt 80×32 lesbar.

Ich hatte max=100 zwischendurch selbst verworfen, weil es "zu tot" aussah — auf
einem Bild, auf dem die Füllung wegen eines CSS-Fehlers gar nicht gezeichnet
wurde. Ein Vergleich mit einem kaputten Bild. Der Fehler: `.spark path
{ fill: none }` schlägt als CSS-Regel das Präsentationsattribut fill="url(#…)".
`fill: none` gehört an die Linie, nicht an jeden Pfad.

Die Ausstattung ist wieder einzeilig. Sechs Felder, sechs Spalten — und die
Bau-Kennung steht nur noch im Titel: als eigene Zeile zwang sie die ganze Tafel
in eine zweite Reihe, für eine Zeichenkette, die fast niemand liest.

Und die Wartezeit: die Seite stößt beim Öffnen eine Sammlung an, wenn noch
keine Messwerte da sind, statt bis zum nächsten minütlichen Lauf leer zu
bleiben. Ein leerer Kasten liest sich als "kaputt", nicht als "gleich". Der Job
ist ShouldBeUnique, zwei geöffnete Seiten reihen also keine zwei ein.

Codex-Befund dazu (P2): das galt auch für Hosts, die der Sammler ohnehin
überspringt — ein Host mitten in der Übernahme hat keinen Token, und seine
Seite hätte die ganze Flotte abgeklappert und die eigenen Kacheln trotzdem leer
gelassen. Wer gesammelt wird, steht jetzt einmal am Modell (Host::collectable),
gelesen von Job und Seite. Zwei Fassungen dieser Frage waren genau der Grund.

Geprüft: 2307 Tests grün, Pint sauber, Codex ohne Befund. Der Test für den
Maßstab prüft jetzt auch die echten Aufrufstellen und nicht nur das Bauteil —
gegengeprobt durch Entfernen von max=100, dann fällt er. Im Browser beide
Fälle angesehen: ruhiger Host vier flache Linien, ausgelasteter Host lesbare
Form, null Konsolenfehler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:56:10 +02:00
nexxo dc35e5310f Release v1.3.93 — die Seite konnte den Host gar nicht fragen
tests / pest (push) Failing after 9m23s Details
tests / assets (push) Successful in 28s Details
tests / release (push) Has been skipped Details
Auf echter Hardware blieb jede Messkachel leer, während der Host sichtbar
online war und vor zwei Minuten geantwortet hatte. Kein Zufall und kein
Aussetzer: ein Entwurfsfehler.

Nur der queue-provisioning-Container hängt im WireGuard-Netz — NET_ADMIN,
/dev/net/tun, das wireguard-Volume. Der app-Container, der die Konsole rendert,
hat überhaupt keine Route zu 10.66.0.x. Ich hatte den Proxmox-Aufruf in
render() gelegt, also in den einen Container, der die Management-Adresse nicht
erreichen kann. Ein Aufruf von dort konnte nie etwas anderes sein als eine
Zeitüberschreitung.

PingHosts schreibt dieselbe Regel seit Langem in seinen Kopf: "runs on the
provisioning queue, which is where the Proxmox credentials are usable." Ich
habe sie gelesen und nicht angewendet.

Verschlimmert hat es mein eigenes catch (Throwable): der Grund wurde
verschluckt, und die Kachel sagte "keine Messwerte" — ununterscheidbar davon,
dass der Host schweigt. Auf dem Testhost fiel nichts auf, weil TEST-NET
ohnehin nie antwortet.

Jetzt zwei Hälften:

- HostLoadSeries::collect() holt und legt ab, aus Jobs\CollectHostLoad auf der
  provisioning-Warteschlange, minütlich — der Takt, in dem Proxmox einen
  frischen Messwert schreibt. Ein Fehlschlag wird protokolliert, mit Host,
  Node und Grund.
- HostLoadSeries::forHost() liest nur aus dem Zwischenspeicher und öffnet nie
  eine Verbindung. Ein Test hält das mit Http::assertNothingSent() fest.

Der Eintrag lebt fünf Minuten bei minütlichem Sammeln: länger als der Takt,
damit ein ausgefallener Lauf keine Seite leert, die eine Sekunde vorher in
Ordnung war — und kurz genug, dass ein stehengebliebener Sammler die Zahlen
mitnimmt, statt eine alte Stunde als aktuell stehenzulassen.

Der Sammler ist ShouldBeUnique (Codex-Befund, P1). Die provisioning-Warteschlange
ist DIESELBE, auf der Kunden-Bereitstellung läuft; ein stiller Host kostet den
vollen HTTP-Zeitablauf, und ohne diese Sperre stauten sich minütlich neue Läufe
hinter dem alten und verzögerten bezahlte Arbeit. Dasselbe Mittel, das
CollectInstanceTraffic nebenan schon benutzt.

Dazu: der Zustands-Punkt war mit 62 px so groß wie der Speicher-Ring nebenan.
Eine gefüllte Scheibe wiegt optisch weit mehr als ein dünner Ring und erschlug
die Kachel — jetzt 32 px.

Und ein Test, der aus Versehen recht behielt: die Kachel-Prüfung verglich mit
"50", was auch die 500 GB in der Instanzenliste darunter trifft. Sie prüft
jetzt Zahlen, die sonst nirgends auf der Seite vorkommen.

Noch offen, nicht hier angefasst: VmTemplateCheck fragt die Proxmox-API
ebenfalls aus dem app-Container heraus, von der Bereitschaftsseite aus. Selber
Fehler, Bestand, eigener Punkt.

Geprüft: 2299 Tests grün, Pint sauber, Codex ohne Befund. Im Browser mit
eingespielten Messwerten: sechs gefüllte Kacheln, Zustands-Scheibe in
Proportion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:23:00 +02:00
nexxo 589489a9fc Release v1.3.92 — sechs Kacheln statt dreier ungleicher Kästen
tests / pest (push) Failing after 10m15s Details
tests / assets (push) Successful in 28s Details
tests / release (push) Has been skipped Details
Nach der Vorlage des Besitzers. Zustand, Speicher und die gemeinsame Kurve
standen nebeneinander: zwei davon halb leer, die dritte überfüllt, und die
gemeinsame Kurve brauchte eine Legende, um zu sagen, welche Linie welche ist.

Eine Reihe je Kachel löst alle drei Beschwerden auf einmal. Die Beschriftung
der Kachel benennt die Reihe, also braucht es keine Legende mehr. Die Höhen
sind durch das Raster gleich statt zufällig. Und der leere Platz ist mit
Messwerten gefüllt, die es ohnehin schon gab: Proxmox' Aufzeichnung liefert
Netzdurchsatz in beide Richtungen mit, ungefragt.

Sechs Kacheln: CPU-Auslastung, RAM-Auslastung, Speicher (als Ring), Eingehend,
Ausgehend, Zustand. Gebaut mit x-ui.metric, x-ui.spark und x-ui.ring — die
gab es alle schon, und der Kopfkommentar von x-ui.metric sagt selbst "exactly
as the approved template draws it". Nichts daneben neu gebaut.

Die öffentliche IP steht jetzt auf der Seite. Sie stand vorher NUR klein unter
der Überschrift — die Adresse, unter der der Host wirklich erreichbar ist, war
in der Detailseite nirgends ein Feld. Sie führt jetzt die Ausstattungs-Tafel
an, und die Reserve-Eingabe ist mit dorthin gezogen: die Kacheln zeigen, was
gemessen wurde, die Tafel, was eingestellt ist. Ein Eingabefeld zwischen
Messwerten sähe aus, als ließe sich eine Messung ändern.

Alle vier Verlaufslinien tragen denselben Ton. Die Regel steht im Bauteil
selbst — "muted where the figure is observed, accent where it can be acted on"
—, und hier ist keine Zahl anzufassen. Vier verschiedene Töne nebeneinander
behaupten einen Unterschied, den es nicht gibt.

Zwei Funde aus der Prüfung
--------------------------

- x-ui.spark warf fehlende Messwerte per array_filter heraus und verband die
  Nachbarn. Zwei Fehler auf einmal: die Linie behauptete eine Messung, die es
  nicht gab, und alles danach rutschte nach links — eine Stunde mit zwei Lücken
  zeichnete sich als achtundfünfzig Minuten. Die x-Lage kommt jetzt aus dem
  Platz in der URSPRÜNGLICHEN Reihe, und zusammenhängende Messwerte werden als
  eigene Züge gezeichnet. Eine saubere Reihe ergibt genau einen Zug und
  dasselbe Bild wie vorher, was alle bisherigen Aufrufer liefern.

- Der Zwischenspeicher überlebt einen Deploy. Ein Eintrag aus v1.3.91 kennt
  netin/netout nicht, und die Host-Seite wäre 55 Sekunden lang an einem
  fehlenden Schlüssel gestorben — genau in der Minute, in der jemand nachsieht,
  ob das Update durch ist. Der Schlüssel heißt jetzt host-load:v2:<id> und
  wandert mit der Form mit.

Und einer, den kein Prüfer gemeldet hat: beim Zerlegen in Züge stand im
Flächenpfad ein `L` unmittelbar vor einem `M`. Gültig gelesen, nicht
gezeichnet — die Füllung verschwand still. Aufgefallen ist es beim Ansehen der
Seite, nicht durch eine Meldung; jetzt prüft ein Test, dass jeder Flächenpfad
mit M anfängt, mit Z endet und keinen Befehl direkt hinter einem anderen trägt.

x-ui.chart behält seinen update-on-Weg, obwohl diese Seite ihn nicht mehr
benutzt: er ist eine geprüfte Fähigkeit des gemeinsamen Bauteils, und der
darunterliegende Fix (Instanz aus dem reaktiven Alpine-Objekt) gilt für jeden
Chart.

Geprüft: 2291 Tests grün, Pint sauber, Codex ohne Befund. Im Browser mit
eingespielten Messwerten: sechs Kacheln, Lücke als echte Aussparung in Linie
UND Fläche, Leerzustand zeigt "—" statt einer Null, null Konsolenfehler über
einen vollen Poll-Zyklus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:02:55 +02:00
nexxo 665a0c4e08 Release v1.3.91 — die Host-Seite zeigt Last statt Ausstattung
tests / pest (push) Failing after 9m29s Details
tests / assets (push) Successful in 20s Details
tests / release (push) Has been skipped Details
Die Karte "Rechenleistung" zeigte keine Leistung. 12 Kerne und 63 GB sind die
Ausstattung des Blechs und ändern sich nie — sie standen aber im selben
Kartenraster wie "Zustand" und "Speicher", die beide leben. Wer die Seite
öffnete, um zu sehen, wie es dem Host geht, las dort eine Zahl, die das nie
sagen konnte.

An ihrer Stelle steht jetzt die Last: CPU und RAM als Stundenkurve, beide in
Prozent auf EINER Achse, dazu die aktuellen Werte als beschriftete Zahlen.

Die Geschichte kommt aus Proxmox' eigener Aufzeichnung
(/nodes/{node}/rrddata), nicht aus einem eigenen Sampler. Ein Sampler hieße
neue Tabelle, minütlicher Job, Aufräum-Job und ~1440 Zeilen je Host und Tag —
um weniger genau nachzubauen, was ohnehin auf der Platte liegt. Die RRD ist ab
der ersten Sekunde gefüllt, auch für die Stunde vor dem ersten Hinsehen, und
kann nicht von dem abweichen, was Proxmox' eigene Oberfläche zeigt.

Eine Lücke bleibt eine Lücke: ein Punkt ohne Messwert wird null, nie 0, und
spanGaps steht auf false. Dieselbe Regel wie in instance_metrics. Antwortet der
Host gar nicht, sagt die Tafel das in einem Satz, statt eine ruhige Stunde zu
zeichnen — auf dem Testhost live bestätigt.

Der Fehler, den das ans Licht gebracht hat
------------------------------------------

x-ui.chart steht überall unter wire:ignore, sonst zerstört Livewire das Canvas.
Ein Poll erreicht den Chart also nie. Dafür bekam das Bauteil ein optionales
update-on: es hört auf ein Fenster-Ereignis und tauscht die Daten IM
bestehenden Chart.js-Objekt.

Das lief nicht. Die Zahlen neben der Kurve wanderten, die Kurve nicht, und
chart.update() starb still im Legenden-Plugin:

    TypeError: Cannot set properties of undefined (setting 'fullSize')

Grund: die Chart.js-Instanz lag als Eigenschaft im Alpine-Objekt und wurde
damit reaktiv umhüllt. Chart.js' Plugin-Innenleben überlebt das Proxy nicht.
Gemessen statt geschlossen: dieselbe Instanz wirft über das Proxy und läuft
über Alpine.raw(). Sie liegt jetzt in der Closure.

Aufgefallen ist es nie, weil bis zum ersten Live-Chart kein einziger Chart in
diesem Repo je update() gerufen hat — konstruieren und Erstzeichnen gehen durch
die Hülle noch. tests/Feature/ChartLiveUpdateTest.php hält die Regel fest,
damit der nächste Live-Chart nicht denselben Nachmittag kostet.

Der Rest
--------

- Version lesbar: "Proxmox VE 9.2.6" statt pve-manager/9.2.6/7f8d…, mitten in
  der Bau-Kennung abgeschnitten. Die Kennung steht klein darunter. Eine
  unerwartete Form wird unverändert durchgereicht statt verschluckt.
- Vier Kleinkarten (Mgmt-IP, Node, Version, Instanzen) sind eine
  Ausstattungs-Tafel geworden. Die Instanzen-Anzahl steht in der Überschrift
  der Liste, die sie ohnehin zeigt.
- Der Übernahme-Fortschritt klappt zu, sobald sie durch ist. Fünfzehn
  abgehakte Schritte sind auf einem laufenden Host kein Dauerinhalt —
  aufklappbar über <details>, ohne JavaScript.
- PlanVersion::requiredTemplateVmids() ersetzt die dritte Kopie derselben
  Fensterlogik.
- BuildVmTemplate sagt nicht mehr "this takes 10–20 minutes". Das war eine
  Schätzung vor dem ersten Lauf; gemessen waren es unter zwei. Eine Konsole,
  die falsch vorhersagt, erzieht dazu, sie zu ignorieren.

Geprüft: 2281 Tests grün, Pint sauber, Codex ohne Befund. Die Farbwahl gegen
den Validator gerechnet (ΔE 28,3 protan / 39,2 normal; der Akzent liegt unter
3:1 gegen die Fläche, deshalb tragen beide Reihen sichtbare Beschriftung). Im
Browser: null Konsolenfehler über einen vollen Poll-Zyklus, und die Kurve
wandert auf dem echten Poll ohne Neuladen — mit eingespielten Messwerten
belegt, weil die Testhosts in TEST-NET liegen und nie antworten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:24:21 +02:00
nexxo ac81bc5b98 Release v1.3.82 — Übernahme-Befehlszeile neu ausstellen
tests / pest (push) Failing after 8m50s Details
tests / assets (push) Successful in 23s Details
tests / release (push) Has been skipped Details
Die Zeile gab es genau einmal: auf der Seite direkt nach dem Anlegen. Wer sie
dort nicht kopierte — oder wem sie wegbrach, wie beim WireGuard-Fehler bis
1.3.80 — hatte danach einen Host in der Liste, für den es keinen Weg zu einer
Befehlszeile mehr gab. Ein zweites Anlegen scheitert an der eindeutigen IP,
also blieb nur Entfernen und von vorn.

HostTakeoverCommand behauptet in seinem Kopfkommentar, die Zeile werde "an
zwei Stellen gezeigt (beim Anlegen und beim Neuausstellen eines Codes)". Das
Zweite gab es nie — ein Kommentar, der eine Absicht beschreibt und wie eine
Beschreibung des Gebauten klingt.

Neu: ReissueTakeover als Modal auf der Host-Detailseite. Bestätigen zuerst
(R23), denn ein bisher ausgegebener Code wird dabei wertlos; danach steht die
ganze Zeile mit Kopieren-Knopf darin, nicht nur der Code.

Codex, zwei Runden, beide Male dieselbe Sorte Fehler von mir — die Ansicht
versteckt den Knopf, die Methode prüft nichts:

- P1: `issue()` erzwingt jetzt selbst, welcher Zustand zulässig ist. Ein
  Modal, das offen blieb, während der Host aktiv wurde, rief die Methode
  trotzdem; eine Livewire-Methode ist ohnehin von aussen aufrufbar.
- P2: `issue()` prüft die Tunnel-Einstellungen erneut. Sonst wird ein gültiger
  Code entwertet und dafür eine Zeile ausgegeben, der die WireGuard-Angaben
  fehlen — genau die kaputte Zeile, vor der die Warnung daneben steht.
- P1 der zweiten Runde: `disabled` gehörte nicht in die Liste der zulässigen
  Zustände. Es sieht aus wie "noch nicht fertig" und ist das Gegenteil —
  toggleMaintenance() schaltet einen LAUFENDEN Host so still. Der Knopf hätte
  eine Produktionsmaschine zur Neuinstallation angeboten.

Die Bedingung steht deshalb einmal am Bauteil (ReissueTakeover::eligible) und
wird von Ansicht und Methode gefragt: zwei Fassungen liefen auseinander,
sobald jemand eine ändert.

2231 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 01:00:53 +02:00
nexxo 435a202fdd Match the panel to the approved template, measured rather than assumed
tests / pest (push) Successful in 8m49s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details
The panel had been built by reading the template and believing the result.
Every round of review found the same class of defect, so this round the
template was rendered in a browser and measured with getComputedStyle, and the
implementation measured the same way — two number columns instead of an
opinion.

That immediately settled a disagreement: a review of the template's SOURCE said
the metric grid used a 20px gap. Rendered, it is 14px. The measurement wins.

Brought to the template's figures: label 11.5px (it was 11px in three separate
places, which is how it drifted — it is now one .lbl rule), value tracking
-0.02em, unit weight 450, metric row 6px above and 14px between, page head
centred with a 14px gap, grid gap 14px, columns switching at 1101/561px and the
h1 at 901px to match the template's own breakpoints, ring 62px, bar 5px.

The shared button now takes its height from min-height (40px) rather than
vertical padding, at 14px/600 with 0 18px padding and a 10px radius — a button
keeps its height whatever sits inside it, icon, spinner or bare text.

Also here, from the same round of review:

- The chart's blue border was Tailwind's own `ring` utility colliding with a
  component class of the same name. Renamed to `.metric-ring`. This was
  dismissed once as a screenshot artefact; it was real.
- Page titles lost the `sm:text-3xl` (40px) an earlier bulk edit had appended
  to 23 of them. The template's h1 is 30px.
- The users table has its actions back — edit, suspend, lock and delete — for
  every seat that is not the owner, with the owner refused in the action itself
  and not merely hidden in the markup.

DemoCustomerSeeder writes one complete customer as real rows: instance,
subscription, six seats across four roles, a backup, current-period traffic and
thirty days of samples with a deliberate wobble and one day at 286/288 checks.
Nothing in the panel is drawn from a fixture any more, so anything missing
shows up as missing. Removing the demo is one deletion.

Verified: 607 tests, and a Codex comparison of the two measurement sets —
"a person would call them the same design", the only difference a 1px gap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 17:08:05 +02:00
nexxo 21d8dc310a One definition of the navigation, and the country instead of the building
The breadcrumb said Übersicht on every page. The sidebar and the breadcrumb
each built their own copy of the structure, so the layout had no way to know
which entry was current and fell back to the first one. Navigation::portal()
and ::console() are now the single definition, and currentLabel() matches on the
route name — which matters for the console, whose PATH changes between
host-bound and fallback mode while its route names do not.

The datacentre name is out of everything a customer sees, for the third time
and now at the source rather than in a template. Falkenstein is how an
operator places an instance; a customer's processing record names the
jurisdiction, and putting the building in customer copy means editing that copy
every time the estate grows.

Also swept the views onto the new scale: --text-faint is 2.8:1 and a decoration
token, but it was carrying table headers, hints and timestamps — text people
have to read. All of it moved to --muted, which is 5.0:1 and passes AA.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 16:09:39 +02:00
nexxo ef110b06db feat(admin): host reserve edit + maintenance drain; customer suspend/reactivate
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 14:30:21 +02:00
nexxo d3d686e575 feat(admin): scalable host list (search/filter/health) + host detail redesign
- host list: search + datacenter/status filters, dense table with heartbeat
  health dot, instance count, capacity meter
- host detail: health hero (last_seen), storage/compute breakdown, technical
  facts, hosted-instances table
- Host model: usedPct() + healthState() helpers

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 14:27:53 +02:00
nexxo bc278c8fa1 feat(admin): datacenters management (create + select)
datacenters table/model + admin CRUD (/admin/datacenters, nav). HostCreate
picks from active datacenters (validated); hosts show the datacenter code.
Seeded fsn/hel. 5 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:25:17 +02:00
nexxo 8d12a40a42 feat(admin): host onboarding UI (add / live stepper / retry / remove)
Real hosts list, add-host form (StartHostOnboarding), host detail with live
progress stepper (Reverb + wire:poll fallback), retry failed run, remove via
wire-elements/modal (deregister only). DE+EN. 8 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:12:41 +02:00