Commit Graph

17 Commits (7224825db1fafe033d728bf2887660de8d619e10)

Author SHA1 Message Date
nexxo 4fc1ccd3db Terminal: die Bruecke ueberlebt jetzt ein Deployment
Aus dem Gesamt-Review, und der erste Befund ist der, der still zugeschlagen
haette.

`terminal` lebt im Netz-Namensraum von `queue-provisioning`, und ein Prozess
bleibt in dem Namensraum, in dem er gestartet ist. `update.sh` startet den Hub
neu — danach lauscht die Bruecke in einem, den es nicht mehr gibt. Nichts meldet
dabei einen Fehler: `docker compose ps` sagt weiter "healthy", weil die
Lebendpruefung ueber Loopback INNERHALB des verwaisten Namensraums laeuft. Nach
aussen antwortet nginx mit 502, und der Betreiber liest "Keine Verbindung —
laeuft der Terminal-Dienst?", waehrend der Dienst behauptet, es gehe ihm gut.
Genau dieselbe Falle, die zwei Bloecke tiefer schon fuer vpn-dns/vpn-gateway
behandelt ist; die Bruecke fehlte in der Behandlung.

Nachgemessen statt geglaubt: Hub neu gestartet -> Docker sagt "healthy", curl aus
dem Namensraum bekommt gar keine Antwort. Nach `restart terminal`: 200.

Und ein zweiter Ausrollfehler daneben: gebaut wurde nur `app`. `docker compose
up -d` baut nur Images, die es noch GAR NICHT gibt — beim ersten Ausrollen faellt
das nicht auf, danach nie wieder. Eine Aenderung an docker/terminal/ saehe
ausgeliefert aus, und es liefe das alte Image.

Ausserdem:
- Die Meldung zu 4502 zaehlte zwei Ursachen auf, der Code deckt fuenf. Die
  Bruecke schickt 4502 fuer JEDE gescheiterte Anmeldung, auch fuer einen
  abgewiesenen Schluessel — und das ist der wahrscheinlichste Fall, wenn ein Host
  neu aufgesetzt wurde. "antwortet nicht" war dort schlicht falsch: die Maschine
  hat geantwortet und abgelehnt. Titel und Text legen sich nicht mehr fest.
- R19: der Kommentar an der Kopfzeile der Spalte nannte "Berechtigung,
  Betriebsbereitschaft" als Grund, warum der Knopf nicht ueberall steht.
  Letzteres entscheidet seit dem Entsperren nichts mehr, und zwanzig Zeilen
  tiefer begruendete der Kommentar am Knopf ausfuehrlich das Gegenteil.
- Der Kommentar am Retry-Knopf erklaerte die Reihenfolge von .hidden gegen
  .inline-flex fuer zu unsicher, waehrend die Buehne dreissig Zeilen hoeher genau
  darauf baut. Tailwind gibt .hidden als letzte Display-Klasse aus; der wahre
  Grund fuer den Wrapper ist, dass die Klassen des Knopfes aus einem geteilten
  Bauteil kommen.
- REDIS_URL stand fest auf Datenbank 1, waehrend PHP REDIS_CACHE_DB liest. Wer
  die anfasst, legt auf der einen Seite ab, wo die andere nicht sucht.

2507 Tests gruen, compose config und bash -n sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 19:41:33 +02:00
nexxo f64a564c40 Terminal: keine Fehlerseite mehr, ein Knopf auf jeder Zeile, eine Buehne statt roter Zeilen
Drei Dinge, die beim ersten Hinsehen im Betrieb auffielen.

1. Der Knopf war weg. Er verschwand, wenn dem Host die Tunneladresse oder der
   Fingerabdruck fehlte — mit der Begruendung, ein Knopf, der verlaesslich in
   eine Ausnahme laeuft, sei schlechter als gar keiner. Das stimmte, solange die
   Seite dahinter mit Laravels Fehlerseite aufging. Jetzt steht er auf jeder
   Zeile: ein fehlender Knopf sah aus wie "hier gibt es kein Terminal" statt
   "hier noch nicht, und zwar deshalb".

2. Die Seite ging mit einem Stacktrace auf. `TerminalTicket::issue()` warf,
   niemand fing es, und wer den Knopf drueckte, bekam Klassenname, Dateipfad,
   Zeilennummer und Quelltextauszug in einem Fenster des eigenen Produkts.
   `blocker()` beantwortet die Frage jetzt VOR dem Ausstellen und gibt ein
   Merkwort zurueck, keinen Satz — die Formulierung gehoert in die
   Sprachdateien. `mount()` wirft nicht mehr, mit Fangzaun fuer das, womit
   niemand gerechnet hat.

3. Der Abbruch war die einzige ungestaltete Stelle im Produkt: eine rote
   ANSI-Zeile mitten in der eigenen Ausgabe. Der Vorspann und der Schirm waren
   Geschwister, von denen abwechselnd eines `hidden` trug — das trug genau
   einmal, beim Aufbau, und fuer alles danach fehlte die Rueckfahrkarte. Die
   Buehne liegt jetzt UEBER dem Terminal und kann dreimal auftreten: beim
   Verbinden, beim Ende, beim Abbruch. Die Sitzung darunter bleibt stehen.
   Welcher Text, entscheidet der Schliesscode der Bruecke (4401 Ticket, 4502
   kein SSH); dazu ein Knopf, der neu laedt, weil ein Ticket dreissig Sekunden
   gilt und genau einmal.

Beim Hinsehen gefunden, nicht beim Testen:
- Die Schriftgrafik war unlesbar. Die Figlet-Zeichnung setzt darauf, dass der
  Unterstrich einer Zeile den Strich der naechsten beruehrt; in IBM Plex Mono
  sitzt er tiefer. Eng verschmierte das Wort, weit zerfiel es. Vollbloecke
  fuellen ihre Zelle und stapeln in jeder Schrift.
- Dunkelrot auf Fast-Schwarz hatte kaum Kontrast. Die Wortmarke bleibt jetzt
  immer in der Akzentfarbe — sie ist keine Statuslampe, was los ist, sagt die
  Zeile darunter.
- Auf dem Schirm stand ":host antwortet nicht". Der Name war an die Erklaerung
  uebergeben, an die Ueberschrift nicht. Ein Test mit
  `toContain(__('...title'))` haette das nie gefunden — er verglich ":host" mit
  ":host". Der neue prueft das Ergebnis.

Nachgewiesen: Knopf oeffnet ein NEUES Tab (die Liste bleibt stehen), Ticket
ausgestellt, Socket verbunden, Bruecke kommt nicht auf den Host, schliesst 4502,
Buehne kommt mit "pve-fsn-1 antwortet nicht" und Knopf zurueck, Knopf laedt
wirklich neu und holt ein frisches 64-Zeichen-Ticket. Der Fingerabdruck dafuer
war geliehen und ist wieder entfernt.

2507 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 19:27:35 +02:00
nexxo 4e52c35f24 Terminal Fix-Runde 1: Ticket-Zusicherung im Test, ehrliche Kommentare, ein Ereignis eine Meldung
- Der Vorspann-Test prüft jetzt data-terminal-splash und die Ticketform
  (64 Hexzeichen) selbst, statt sich auf ein aria-label zu verlassen, das
  nur wegen des Tests existiert.
- Neuer Test: Berechtigung vor Nachschlagen — eine erfundene UUID meldet
  403, nie 404.
- Der Knopf-Sichtbarkeitstest sichert jetzt auf die konkrete Terminal-Route
  zu statt auf das nackte Wort "Terminal" irgendwo auf der Seite.
- Drei Kommentare (terminal.js, bare.blade.php, vite.config.js) behaupteten,
  die Seite liefe ohne Livewire/Chart.js — sie ist aber eine Vollseiten-
  Livewire-Komponente und zieht app.js über <x-shell.head> ohnehin mit.
  Kommentare korrigiert: eigener Einstiegspunkt, damit der Terminalcode
  nicht in app.js landet, nicht weil die Seite ohne Livewire liefe.
- terminal.js: ein fehlgeschlagener Socket feuert error UND danach close;
  onclose schweigt jetzt, wenn nie ein Byte ankam, statt "Verbindung
  beendet" hinter "Verbindung nicht möglich" zu schreiben.
- terminal.js: Textrahmen landen jetzt als String im Terminal statt als
  leeres Uint8Array.
- data-host wird jetzt gelesen und steht in den Verbindungsmeldungen.
- wire:ignore auf dem Terminalschirm, bevor die Komponente ihre erste
  Aktion bekommt und xterms DOM beim nächsten Render löscht.
- hosts.blade.php/host-detail.blade.php: der Terminal-Knopf trägt sein
  href jetzt selbst (x-ui.button :href), statt in einem <a> zu stecken —
  interaktiver Inhalt in einem Link war ungültiges HTML.

Suite: 2502 bestanden (vorher 2501 + ein neuer Test).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 18:32:58 +02:00
nexxo 941950584c Terminal: eigenes Fenster mit Vorspann, Knopf in Liste und Detailseite
Aufgabe 2: alles, was der Betreiber sieht, noch ohne Container dahinter.
Der Knopf bleibt fuer einen Host ohne Tunneladresse oder Fingerabdruck
absichtlich unsichtbar, statt in eine unbehandelte RuntimeException aus
Aufgabe 1 zu fuehren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 18:18:22 +02:00
nexxo 325e69f8f0 Host-Liste holt sich ihren Stand selbst; die Ausstattung braucht keine Zeile nur fuer die Adresse 2026-08-02 17:00:30 +02:00
nexxo 2c30d1fb86 Host anlegen: die Seite beschrieb das Rettungssystem und startete die Pipeline 2026-08-02 01:38:48 +02:00
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 398028a57d Rebuild Add host as a page with one job, and let downloads through the gate
The page was a wall. Six paragraphs of procedure stacked above a form, so the
thing the page actually asks for — four fields — sat underneath an essay about
what would happen afterwards. It answered everything and showed nothing.

It is now built the way the settings page is built, because that page was
rebuilt for the same reason and there is no case for a second idiom: an eyebrow,
a title, a sticky rail on the left and panels of rows on the right.

The rail carries the six steps as two-word labels rather than paragraphs. That
is what the question at this moment actually is — where am I, how much is left —
and it fits in 232 pixels. The numbers carry the state; a tick beside them would
be a second sign for one statement, and a number can be counted.

What a step MEANS now appears in the row where it is due, not six times in
advance. The two provider steps get a panel of their own above the form, because
they have to be done before you save: the one-time code starts expiring the
moment you do. Everything after the form waits until there is something to say.

After saving the command becomes the page. Full width, its own framed block with
the warning in the header strip rather than floating above, and the two
remaining steps below it as ordinary rows.

Two token bugs went with it. `bg-canvas` does not exist — it was a class that
compiled to nothing, which is part of why the block looked wrong. And
`text-accent` on white is 2.9:1; the config says in as many words to use
accent-text for anything read, so the current step number does.

Also fixed, and it would have broken every takeover in production: PublicSiteGate
is appended to the whole web group, so while the site is hidden a server in a
rescue system fetching the archive would have received the 503 placeholder and
piped it into tar. The operator would have seen an unpack error on a machine
only reachable through the provider's console, with nothing pointing at a switch
in the admin area — and they are by definition neither on a management network
nor signed in, since leaving that state is the whole point. Exempted by ROUTE
NAME, not by hostname: the docblock rightly warns that exempting by host means
trusting a header the caller picks, but that warning is about the entire portal.
Behind these two routes are documents that are public anyway and an archive that
404s without a valid one-time code.

2026 tests pass, assets build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 21:51:30 +02:00
nexxo 2194ab8079 Put the instructions where somebody reads them before they start
The previous commit shipped the guide AFTER the host was created. Its first two
steps happen at the provider — order the machine, boot the rescue system — and
whoever reads them there has the one-time code already in the clipboard with its
24-hour window running. The instructions were correct and in the wrong place,
which is its own kind of wrong.

Six steps now, not three, and the two provider ones are named rather than
assumed. They are marked "first" so the list does not read as something you can
start at the top of and work down: step 2 is half an hour of waiting, and doing
it inside a running code is exactly the mistake the page should prevent.

All six are visible the whole time, including the done ones and the ones still
to come. A guide that shows only the current step cannot answer "how much is
left", which is the question somebody has while a server they are paying for
sits in a rescue system.

It is on the hosts list too, collapsed. Somebody standing there may not have
ordered the machine yet — and that is step 1. Requiring them to click "add host"
to find out what the procedure is puts the answer behind the action it describes.

One component, two places, so the archive address in the instructions is the same
string the command line will carry. Two copies of that would drift, and the
difference would surface on a server that has already been paid for.

The step-4 command box moved into the guide rather than sitting beside it, so
after creating a host the whole procedure stays on screen. Somebody stuck at
step 5 should not be looking at a page that has become a single box.

1991 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 20:51:47 +02:00
nexxo 7a5a71dc89 Read VPN host state from the synced peer table, not a live hub call
tests / pest (push) Failing after 7m20s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details
2026-07-29 00:18:21 +02:00
nexxo eb743d34ed Show WireGuard tunnel state per host in the hosts list 2026-07-29 00:10:15 +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 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
nexxo 1aa7535fc4 feat(admin): dark Tactical-Terminal operator console
Separate admin console at /admin, gated to is_admin users (EnsureAdmin middleware
+ migration/seed; a plain customer user is seeded to prove the gate). Distinct dark
aesthetic achieved purely by token scoping: .theme-admin overrides every CSS design
token to a dark graphite / signal-orange palette, so ALL shared components (button,
card, badge, table, stat, chart) render dark with zero new markup (R3). Chart island
now reads tokens from its own element, so charts are theme-aware.

Sections (each full-page class-based Livewire, English routes R13, localized DE/EN):
- Overview: fleet KPIs, fleet-growth line, host-load bars, MRR bars, active runs, alerts.
- Customers: table + plan doughnut. Instances: fleet table (vmid/host/storage).
- Hosts: capacity cards (storage/CPU bars). Provisioning: runs table + live stepper.
- Revenue: MRR/ARR/ARPU/churn KPIs, MRR line, plan doughnut, recent payments.
- Locale-aware month labels/currency (Carbon/Number).

18 new Pest tests (guest redirect / non-admin 403 / admin render per section) → 44
green. R12 browser: all six admin pages HTTP 200, ZERO console errors (Chart.js dark).
Codex (R15) — clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 08:35:03 +02:00