Commit Graph

33 Commits (3e1977804679d2f2d49181b2aa2f4ab62f2e857f)

Author SHA1 Message Date
nexxo a9fbb395f9 feat(engine-b): live provisioning progress (admin console + customer dashboard)
Admin /admin/provisioning bound to real runs+steps (poll+admin.runs). Embedded
CustomerProvisioning card shows the logged-in customer their own run live
(per-customer StepAdvanced channel, email-bridged authz). Shared BuildsRunSteps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:57:35 +02:00
nexxo 65ba3d6588 feat(engine-b): Stripe webhook -> idempotent customer provisioning intake
Signed webhook (HMAC verify when secret set), CSRF-exempt route. Paid checkout
creates customer+order+run (stripe_event_id unique => duplicate webhooks start
one run) and dispatches. 5 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:52:46 +02:00
nexxo 446da58061 feat(engine-b): 15-step customer pipeline + DNS/Traefik services
validate->reserve(placement)->clone->cloudinit->start->guestagent->network->
deploy->nextcloud->admin->dns/tls->backup->monitoring->acceptance->complete.
HetznerDnsClient + TraefikWriter (interface+fake+real), CloudReady notification,
hosts.node (set by RegisterCapacity). Secrets transient/encrypted, never
plaintext. 21 tests incl. mocked end-to-end + crash idempotency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:50:26 +02:00
nexxo e5aeb3989e feat(engine-b): ProxmoxClient VM-lifecycle extension (interface + fake + http)
nextVmid/cloneVm/setCloudInit/resizeDisk/startVm/vmStatus/guestAgentPing/
guestExec/taskStatus/applyFirewall. Fake is deterministic with configurable
task/guest failure hooks; Http polls UPID + guest exec-status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:41:48 +02:00
nexxo 770a6cf7cd feat(engine-b): CustomerStep base + shared resource trait + config
ManagesRunResources trait (host+customer), CustomerStep base (order/instance/
plan/guest helpers), config plans + 15-step customer pipeline + dns/traefik.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:39:56 +02:00
nexxo 406251dfde feat(engine-b): customer domain models + migrations
customers, orders (ProvisioningSubject, stripe_event_id unique), instances
(subdomain unique, encrypted nc_admin_ref), dns_records, backups,
monitoring_targets, onboarding_tasks. Host capacity: committedGb/availableGb +
datacenter placement. 7 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:38:01 +02:00
nexxo c7fb1ce56d fix(engine): dispatch run continuation after releasing the lock
Prevents a second worker from consuming the follow-up job and bailing on the
still-held run lock, which would stall the run until the next scheduler tick.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:09:24 +02:00
nexxo 294c9aef2f fix(engine): store wg_pubkey right after addPeer so failed peers are cleanable
Keeps the wg_peer resource gated on a verified handshake (idempotency) while
ensuring PurgeHost can always remove the hub peer, even on terminal failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:07:41 +02:00
nexxo 9e664654a2 fix(engine): record wg peer only after verified handshake; async host purge
- ConfigureWireguard checks setup command results and only records the wg_peer
  resource after the handshake verifies, so a transient setup failure re-runs
  the full setup instead of getting stuck on the idempotent path.
- Host removal now deactivates immediately and queues PurgeHost, which deletes
  runs under the runner lock (waiting out a long step) — no 15s LockTimeout error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:06:13 +02:00
nexxo a6a41719bb fix(engine): long SSH command timeout; wg peer removal on privileged worker
- PhpseclibRemoteShell sets a command timeout (default 2000s) so apt
  full-upgrade/install don't hit phpseclib's ~10s default and fail.
- Host removal dispatches RemoveWireguardPeer to the provisioning queue, which
  runs in the worker that owns wg0 (the web container can't manage WireGuard).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:01:02 +02:00
nexxo 5979470dda fix(engine): install wireguard-tools + bring up wg0; lock-coordinate host removal
- Image installs wireguard-tools/iproute2; the provisioning worker brings up
  wg0 before queue:work so LocalWireguardHub can manage peers.
- ConfirmRemoveHost deletes each run under its run:<uuid> lock so an in-flight
  worker can't keep mutating the server or write to a deleted run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:57:51 +02:00
nexxo 93191f5de9 fix(engine): fail on unresolvable pipeline step; make worker the WG hub
- RunRunner catches step-resolution errors (removed/renamed pipeline) and
  fails the run terminally instead of looping forever every tick.
- queue-provisioning worker gains NET_ADMIN + tun + persistent wireguard
  volume + published UDP port so LocalWireguardHub can manage wg0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:53:40 +02:00
nexxo 8a392bfd06 fix(engine): parse WireGuard CIDR for address allocation (any prefix length)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:48:16 +02:00
nexxo b65fe69088 fix(engine): drop wg peer on host removal; clear reboot state on deadline fail
- Removing a host now removes its WireGuard hub peer so a freed wg_ip can't
  route to the removed server via a stale peer.
- RebootIntoPveKernel clears reboot_issued/deadline when it times out, so the
  manual retry action actually re-issues a reboot instead of failing instantly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:46:11 +02:00
nexxo 91e308efb0 fix(engine): one host per public IP (unique constraint + validation)
Prevents two onboarding runs from fighting over the same physical server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:43:30 +02:00
nexxo 48ed5e7d34 fix(engine): fail if the WireGuard peer can't be persisted (wg-quick save)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:41:11 +02:00
nexxo f240401a42 fix(engine): honour configured WireGuard prefix length in wg0.conf
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:39:46 +02:00
nexxo 18d1ed2424 fix(engine): derive WireGuard hub IP from the configured subnet
hub_ip is now defined in config (default: subnet .1), so a custom
CLUPILOT_WG_SUBNET no longer pings the wrong handshake target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:38:17 +02:00
nexxo ced7e6103d fix(engine): durable queueing for long provisioning steps
- Dedicated 'provisioning' queue connection (retry_after 2400) + worker
  (--timeout=2100); AdvanceRunJob tries=1, timeout=2100 on that queue.
- Run lock TTL raised to 2100s so a long step can't be run concurrently by a
  duplicate tick-dispatched job.
- StartHostOnboarding creates host+run in a transaction, dispatches after commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:36:49 +02:00
nexxo fbe87d99d5 fix(engine): honour backoff timing + don't double-count Proxmox storage
- RunRunner returns early for a waiting run whose next_attempt_at is in the
  future, so stale/duplicate jobs can't bypass backoff.
- RegisterCapacity takes the largest VM-capable datastore instead of summing
  overlapping pools (local + local-lvm), preventing placement overcommit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:32:14 +02:00
nexxo 8a2a2ee695 fix(engine): reset step timer on retry so timed-out steps recover
A retried step (manual or after a timeout) now gets a fresh started_at, so
RunRunner no longer immediately re-detects the timeout and burns the retry
budget without executing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:29:11 +02:00
nexxo 62c4412623 fix(engine): address Codex round 2 (poll budget, host error state, wg race)
- StepResult::poll — polling steps (reboot) wait without consuming the retry
  budget; the step owns its deadline. Reboot maxDuration > deadline.
- Failed runs move a Host subject to 'error' via ProvisioningSubject hook
  (no host stuck 'onboarding').
- ConfigureWireguard allocates + reserves the wg_ip under a global lock;
  unique index on hosts.wg_ip as a backstop against duplicate addresses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:26:37 +02:00
nexxo 65717bd3bd fix(engine): address Codex review (auth token bootstrap, tunnel recheck, ssh)
- CreateAutomationToken now bootstraps the pveum role/user/token over the
  authenticated SSH session (a fresh host has no API token yet); ProxmoxClient
  is read-only in A.
- ConfigureWireguard re-verifies the handshake on the idempotent replay path,
  never advancing over a dead tunnel.
- PhpseclibRemoteShell treats a missing SSH exit status as failure (255).
- connectWithKey verifies the pinned host key fingerprint on later logins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:20:59 +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 ac3d17cd6a feat(engine): 11-step host onboarding pipeline (SSH -> WG -> Proxmox)
Idempotent steps: validate, ssh-trust (deploy key + scrub password),
prepare base, wireguard (hub peer), install proxmox-ve, reboot-into-pve
(retry-poll), configure, automation token, verify api, register capacity,
complete. StartHostOnboarding action. 22 tests incl. mocked end-to-end +
crash idempotency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 10:04:39 +02:00
nexxo c06ca0ae5d feat(engine): SSH / WireGuard / Proxmox service layer
RemoteShell (phpseclib + fake), WireguardHub (local wg + fake), ProxmoxClient
(REST + fake). Interfaces bound in AppServiceProvider; tests swap fakes via
fakeServices() helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 09:58:02 +02:00
nexxo 4f724d57c9 feat(engine): orchestrator core (state machine + tick + lock)
StepResult, ProvisioningStep contract, PipelineRegistry, RunRunner (per-run
lock, advance/retry/fail, backoff, timeout, append-only events + Reverb
StepAdvanced), AdvanceRunJob (provisioning queue), minutely Tick, admin.runs
channel. 21 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 09:54:52 +02:00
nexxo 11cdcdd4da feat(engine): core provisioning data model + hosts
hosts, provisioning_runs (polymorphic), append-only provisioning_step_events,
run_resources (idempotency breadcrumbs). Models with encrypted api_token_ref,
json context helpers, UUID routing (R11).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 09:49:43 +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
nexxo c4cff8f67c feat(portal): full sidebar — Cloud, Users, Backups, Invoices, Support
Each sidebar tab is now a full-page class-based Livewire component (R1/R2) with an
English route (R13), localized DE/EN (R16), Chart.js islands and token styling:
- Cloud: instance details + specs + storage-over-time line chart + actions.
- Users: user table (roles/groups/status badges) + users-by-group doughnut.
- Backups: 14-day size bar chart + backup history table + restore.
- Invoices: invoice table + next-charge card + monthly-spend bar chart (locale-
  aware month labels, Number::currency amounts).
- Support: contact cards + tickets table + FAQ accordion (Alpine).
- Sidebar links all tabs with routeIs() active state; global toast in app shell.
- All fixture dates/numbers locale-aware (Carbon isoFormat / Number::format).

12 new Pest tests (guard + render per tab) → 26 green. R12 browser: all six tabs
HTTP 200 with ZERO console errors (Chart.js clean). Codex (R15) — clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 08:08:33 +02:00
nexxo 499adaff00 feat(dashboard): rich overview with Chart.js (customer-portal template)
- Chart.js as an Alpine island (x-ui.chart): configs are PHP arrays; colours use
  token: strings resolved from CSS design tokens at runtime (R3). Respects
  reduced-motion; IBM Plex chart defaults.
- Tokens: soft --radius-xl (20px), --success-bright for dots/rings/charts.
  Staggered 'rise' reveal keyframe. Global toast in the app shell.
- Overview rebuilt to the customer template: storage doughnut ring, availability
  sparkline, KPI tiles, upsell, cloud card, interactive onboarding checklist
  (Alpine), backups, modules (Lucide icons — no emoji, R9), activity feed.
- Locale-aware fixture display: Carbon isoFormat dates + Number::format sizes so
  the EN dashboard is not mixed-language (R16).
- 14 Pest tests green; R12 browser: /dashboard 0 console errors with Chart.js;
  R7 no overflow at 375/768/1280. Reviewed with Codex (R15) — clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 07:59:19 +02:00
nexxo 254a7d46a0 feat(portal): Fortify auth + Login/2FA/Dashboard + component kit
Backend (Fortify):
- laravel/fortify with TOTP two-factor + recovery codes; User uses
  TwoFactorAuthenticatable; 2FA credentials hidden from serialization.
- Views off — pages are full-page class-based Livewire components (R1/R2);
  Fortify handles POST actions. Home redirect -> /dashboard. v1 scope: login +
  2FA only (no public register/reset/passkeys). Seeder gated to local/testing.

Component kit (Blade, token-based, a11y):
- button, input, checkbox, alert, card, badge, stat-tile, otp-input (Alpine,
  auto-advance/paste, -safe submit), progress-stepper, nav-item, icon
  (Lucide), plus layouts/portal-app app-shell (sidebar drawer + topbar + menu).

Screens (localized DE/EN, R16):
- Login (form -> login.store), Two-factor challenge (OTP + recovery fallback),
  Dashboard (KPI stat tiles, instance card, provisioning stepper fixtures,
  activity). Routes English (R13).

Tests + verification:
- Pest: 14 green (login ok/invalid/throttle, dashboard guard, component render).
- R12 browser (Puppeteer, prod assets): /, /login, /two-factor-challenge and
  the authenticated /dashboard all HTTP 200 with ZERO console errors; login
  flow verified end-to-end.
- Test isolation fixed (force test env over injected .env).
- Reviewed with Codex (R15): 4 rounds, all findings fixed, final pass clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 01:20:25 +02:00
nexxo 2ae7595aff chore: bootstrap CluPilot control-plane (Laravel 13, Docker stack)
- Docker dev stack: app (php-fpm+nginx+vite via supervisor), reverb, queue,
  scheduler, mariadb 11.4, redis 7 — env-driven ports/UID (HOST_UID=1000).
- Laravel 13.8 + Livewire 3.8 (pinned v3 per R2), Tailwind v4, Reverb, Echo,
  phpseclib, wire-elements/modal.
- .env wired to service names; DE default locale (R16); browser Reverb host
  env-driven; self-hosted-fonts-ready vite.config (R14).
- Entrypoint bootstraps vendor/node_modules on fresh checkout; bin/clupilot
  helper (UID/port env-driven) + shell aliases.
- Reviewed with Codex (R15): 7 findings fixed, 1 verified false-positive.

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