Commit Graph

85 Commits (3b288486cd5cc580edaf9ae0224839a86b94f763)

Author SHA1 Message Date
nexxo 3b288486cd feat(portal): seats management — invite/role/revoke against plan limit
- seats table + model; Users page manages team members (owner auto-created)
- invite respects plan seat allowance + dedupe; role change + revoke guarded
  so the last owner can never be removed or demoted

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 14:40:18 +02:00
nexxo df10448e5d feat(portal): settings page — company profile, branding (logo+colors), cancel package, close account
- customers gain profile + branding + closed_at; instances gain cancellation
  fields; branding resolver (NULL -> CluPilot defaults) snapshotted into the
  provisioning run context
- cancel package: term-end, irreversible, typed-confirm modal (R5)
- close account: guarded (no active package), typed-confirm modal (R5)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 14:36:52 +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 b44d25404d test: widen DatacenterFactory code space (fix faker unique() pool exhaustion)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 14:23:49 +02:00
nexxo 54d9a05235 feat(admin): datacenter edit+delete (guarded); MRR chart fills card; host-load as meters
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 14:22:15 +02:00
nexxo e3ab3cb501 feat(portal): service framing + gradient line charts + billing button fix
- plans gain seats + performance class; customer views show storage/seats/
  performance/features, no raw vCPU/RAM (admin keeps specs)
- billing: scoped wire:target + wire:key so one purchase no longer spins all
- chart island: line-colour->transparent gradient for filled line datasets;
  MRR / backups / invoices bar charts converted to gradient line

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 14:18:08 +02:00
nexxo a3423a2cee docs: Phase D spec — service framing, redesign & CRUD completeness
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 14:13:23 +02:00
nexxo 80348d6a3f fix: unique customer↔user link + reject inactive datacenters on host create
- customers.user_id unique so impersonation/billing can't cross customers
- HostCreate validates datacenter is active (exists rule + active=1)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:56:07 +02:00
nexxo b44e224bee fix: backfill datacenters on migrate; resolve billing customer by user link
- migration seeds fsn/hel + any host-referenced code so existing installs
  keep selectable datacenters after upgrade
- Billing::customer() resolves via user_id, email only as legacy fallback

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:53:06 +02:00
nexxo 4e86e135a6 fix(admin): harden impersonation + datacenter code per Codex review
- normalize datacenter code before uniqueness validation
- ensureUser: race-safe create + refuse linking admin accounts
- impersonate start/leave are POST (CSRF-protected)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:50:16 +02:00
nexxo 879697c6ea feat(admin): impersonate customer portal — session login + return banner
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:46:48 +02:00
nexxo 515f26234e feat(portal): billing page — current plan, upgrades, extra storage, add-ons
New /billing page + nav; plans gained price_cents, storage_addon + addons
catalogue in config. Purchases create a pending Order intent (fulfillment
mocked). Dashboard storage upsell links here. DE+EN. 6 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:36:52 +02:00
nexxo eabcb98f91 feat(admin): compact redesign of /admin/provisioning current-run panel
Progress bar + X/total, highlighted current step, faint next step, failure
inline — replaces the long repetitive per-step 'done' list. Tactical-Terminal
tokens, DE+EN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:31:49 +02:00
nexxo 7c1a153446 fix(ui): fixed sidebars (admin + portal) + storage banner shimmer
App-shell layout: sidebar is a full-height fixed column on lg, only main
scrolls. Re-added the left->right shimmer sweep on the dashboard storage
banner (reduced-motion safe, token colours).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:28:05 +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 5fb4e637fc docs: panel improvements (phase C) design spec
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:20:55 +02:00
nexxo d7c2edb1df fix(engine-b): idempotent local DNS record (firstOrCreate on record_id)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:08:13 +02:00
nexxo 96aceeeaa1 fix(engine-b): stop customer dashboard polling once provisioning failed
Show the failure state but only poll while pending/running/waiting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:06:31 +02:00
nexxo 66e8a4bbf6 fix(engine-b): clean re-clone on lost task ref; idempotent firewall rules
- Clone recovery: once the lock clears but the task ref is lost, destroy the
  possibly-incomplete VM and re-clone instead of advancing onto it.
- applyFirewall clears existing rules before adding, so retries don't accumulate
  duplicate Proxmox firewall entries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:04:48 +02:00
nexxo 9162446b1f fix(engine-b): acceptance gate verifies real health, not just breadcrumbs
- Nextcloud: parse occ status JSON (installed + not in maintenance).
- Admin: query occ user:info for the account.
- Monitoring: MonitoringClient::isHealthy checks the provider, not the local row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 13:00:49 +02:00
nexxo 491800e09c fix(engine-b): grant VM.Backup to the Proxmox automation role
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:57:11 +02:00
nexxo 157496f0c5 fix(engine-b): write Traefik route to the serving host over SSH, not locally
TraefikWriter now targets the host that serves the traffic (DNS points at it):
SshTraefikWriter SSHes in and writes the file-provider YAML there. Interface
takes the traffic host + guest backend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:55:24 +02:00
nexxo 483a79a822 fix(engine-b): treat unreachable cert endpoint as not-ready (poll, not retry-burn)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:52:37 +02:00
nexxo 03c4b6508f fix(engine-b): Proxmox agent exec repeated command fields; Traefik write throws on failure
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:50:57 +02:00
nexxo 8825f9abfe fix(engine-b): Proxmox config uses PUT; guest-agent exec is form-encoded
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:48:58 +02:00
nexxo 778bb7f117 fix(engine-b): always scrub credentials once delivered; idempotent monitoring lookup
- CompleteProvisioning scrubs admin_password whenever credentials_sent exists,
  so a crash between recording and scrubbing can't leave the password in context.
- HttpMonitoringClient looks up an existing monitor by URL before creating one,
  so a retry after a crashed POST doesn't create a duplicate external monitor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:46:34 +02:00
nexxo 661b52dbe0 fix(engine-b): clone recovery waits for lock; register rows before breadcrumb
- Clone recovery polls until the VM lock clears instead of advancing onto a
  possibly-incomplete clone.
- RegisterBackup/RegisterMonitoring create the local row BEFORE the run-resource
  breadcrumb, so a crash between them can't leave the row permanently missing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:44:22 +02:00
nexxo 3c5a18b5d7 fix(engine-b): apply plan CPU/RAM; idempotent backup + monitoring registration
- ConfigureCloudInit sets cores/memory from the plan (not the template default).
- RegisterBackup uses a deterministic Proxmox job id (ignore-exists) + firstOrCreate.
- RegisterMonitoring is idempotent by URL + firstOrCreate — no duplicate schedules
  or monitors after a crash/retry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:42:10 +02:00
nexxo 5cf1964d7d fix(engine-b): default-deny firewall, disk-based capacity, synchronous credential mail
- applyFirewall sets policy_in=DROP so only 80/443 are exposed.
- Capacity/placement account for disk_gb (the real VM allocation), not the
  smaller Nextcloud user quota — no systematic overcommit.
- CloudReady sent synchronously again; the step retries on mail failure with the
  password preserved (no lost credential in a failed queue job).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:39:15 +02:00
nexxo c79874ccfe fix(engine-b): keep admin password ciphertext in the queued mail; valid backup schedule
- CloudReady receives the ENCRYPTED password (decrypts only when rendering), so
  the queued payload never holds plaintext.
- RegisterBackup uses a valid Proxmox calendar expression (02:00).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:35:49 +02:00
nexxo b4cf94ee1a fix(engine-b): real backup/monitoring registration + OC_PASS scoping
- CreateCustomerAdmin puts OC_PASS on the docker invocation (survives the &&).
- RegisterBackup creates a real vzdump job via ProxmoxClient::createBackupJob.
- RegisterMonitoring registers via a new MonitoringClient service (interface +
  fake + http). Both persist the external id [E] before the breadcrumb, so
  acceptance reflects a real registration, not a fabricated row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:33:45 +02:00
nexxo 64e3ca421d fix(engine-b): async stripe payments, capacity honesty, durable credential mail
- Accept checkout.session.async_payment_succeeded (async methods) and dedupe on
  the checkout session id, not the event id.
- committedGb still counts a failed instance while its VM exists (has vmid);
  releases quota only when no VM was created — no overcommit vs no leak.
- CloudReady is queued (durable). Credential delivery is documented at-least-once
  (a rare duplicate welcome email beats a lost credential; exactly-once = v1.1 outbox).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:29:47 +02:00
nexxo e0e4c4e18d fix(engine-b): run guest-agent commands through /bin/sh -c
The Proxmox guest agent execs a program directly; our compound commands
(cd/&&/pipes/redirects/env) need an explicit shell.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:25:24 +02:00
nexxo f43ade79b0 fix(engine-b): address Codex round 6 (crash-window idempotency)
- Clone: recover via vmExists() when the task ref was lost, instead of
  re-cloning the reserved vmid (which Proxmox rejects).
- Deploy: persist the DB password ENCRYPTED before compose up and reuse it on
  retry, so a crash can't regenerate a mismatching credential.
- Complete: gate credential delivery on a credentials_sent breadcrumb so a
  retry after a crash doesn't re-send the email.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:23:28 +02:00
nexxo e2b8f25e78 fix(engine-b): address Codex round 5 (capacity release, activate-last, guest-ip poll, email unique)
- Failed run releases the instance (status=failed) so its quota stops counting
  against host capacity.
- CompleteProvisioning activates instance+order only AFTER onboarding tasks +
  credential delivery succeed.
- ConfigureNetwork polls until the guest has an address (no wrong Traefik route).
- customers.email unique + race-safe customer resolution in the intake action.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:19:31 +02:00
nexxo c56aa14f9d fix(engine): don't reset started_at on poll so step deadlines accumulate
RunRunner::onPoll no longer resets started_at, so a poll step's own deadline
(WaitForGuestAgent 270s, ConfigureDnsAndTls cert 840s) actually fires instead
of resetting every poll. maxDuration raised above each own-deadline so the
step's fail wins over the generic timeout. Shared core fix (A + B).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:15:26 +02:00
nexxo fffcecb152 fix(engine-b): address Codex round 3 (webhook email/replay/rotation, db secret)
- Require a real customer email (no unknown@ fallback merging customers).
- Stripe signature: enforce 5-min replay tolerance + accept any rotating v1.
- DeployApplicationStack no longer persists the DB password in the run context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:12:24 +02:00
nexxo e6bd6c9354 fix(engine-b): address Codex round 2 (fail-closed webhook, placement lock, clone/admin idempotency)
- Stripe webhook fails closed when the signing secret is missing (outside local/testing).
- ReserveResources places + creates the instance under a per-datacenter lock so
  concurrent orders can't overcommit a host.
- CloneVirtualMachine reserves the vmid + breadcrumb BEFORE cloning, so a crash
  can't mint a new vmid and orphan the first clone.
- CreateCustomerAdmin checks user existence and resets the password instead of
  re-running user:add on retry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:09:21 +02:00
nexxo 42fa92cf20 fix(engine-b): address Codex (paid-only stripe, guest routing, absolute disk)
- Stripe: only checkout.session.completed with payment_status=paid (dedupes the
  paired payment_intent event and blocks unpaid async sessions).
- Capture the guest IP (ConfigureNetwork) and point Traefik at the VM, not the
  Proxmox host, so ACME/HTTP-01 can reach Nextcloud.
- Resize the disk to an absolute target (not '+…') so a retry can't double it.
(Codex #4 timeout was a false positive — started_at resets per step.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:05:31 +02:00
nexxo 3e19778046 chore(engine-b): provisioning step/mail i18n (DE+EN) + demo seed data
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:59:06 +02:00
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 b9742f61ee docs: customer pipeline (Subsystem B) implementation plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:35:28 +02:00
nexxo 5e6d6b11b6 docs: engine customer-pipeline spec (the missing Section 3, aligned to the built core)
Detailed build contract for Subsystem B (15 customer-provisioning steps),
written against the real Subsystem-A contracts: ProvisioningStep/StepResult
(advance/retry/poll/fail), RunResource idempotency breadcrumbs, ProvisioningRun
context, the config/provisioning.php pipeline registry, and the read-only
ProxmoxClient it must extend. Covers new models (customers/orders/instances/…),
ProxmoxClient VM-lifecycle additions, Hetzner-DNS + Traefik services, Stripe
idempotent intake, live progress binding to the existing admin/customer views,
build order and DoD. Unblocks B.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 11:27:28 +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