12 Commits (c899d41946ffa85f8eda02c90afb7b7c16907f2f)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
9b8d5dfd1e |
Editing in modals, an update button that is not gated on a stale reading, and a support page that is real
tests / pest (push) Successful in 7m18s
Details
tests / assets (push) Successful in 19s
Details
tests / release (push) Successful in 3s
Details
Three things reported together. ── Editing belongs in a modal (R20) The seats table grew its input fields into the row. It worked and it looked broken: the row grew, the columns beside it jumped, and a table half in edit mode reads as a rendering fault rather than as a form. The project already had the answer — EditDatacenter, whose own header comment says it avoids exactly that row-height jump — and the seats table simply did not use it. EditSeat is now a ModalComponent. A modal is reachable WITHOUT the page's route middleware, so it resolves the customer itself and re-reads the record rather than trusting a hydrated property: a forged addressEditable would otherwise open the address of an accepted seat, and the address is the person — editing it hands one employee's access to another with nobody told. Only an invitation still in flight can have its address corrected, which is the case that actually comes up. The actions column is now always drawn. It used to disappear when the only seat was the owner, on the reasoning that there was nothing to act on. But every seat can be renamed, and a column that vanishes does not read as "not applicable here" — it reads as "this product cannot do that", which is how it was reported, three times. The owner's row says "Geschützt" rather than leaving an empty cell. Also caught here: the edit fields carried class-wide #[Validate] attributes, so an empty edit form made the INVITE button fail on a field the invite form does not have. Rules for an action belong to the action. ── The update button "I cannot run an update, it says everything is current." `behind` is a READING taken by the agent every five minutes, not the state of the world — push a commit and the console insists it is up to date and refuses to act. The agent does its own fetch before deciding, so asking against a stale reading costs one fetch and finds nothing; being locked out costs the deployment. The button is now offered whenever an agent is alive and no run is in flight, labelled for what it does rather than for what the last reading said. ── The update never announced that it had finished Because the thing being watched restarts the thing doing the watching. Mid-run every wire:poll request fails, and what answers afterwards is a new build being questioned by the old page's JavaScript — so the card sat on "läuft" until somebody reloaded by hand. A small Alpine watcher now asks a plain JSON endpoint (no Livewire, no component state, no assets), treats a failed request as the restart rather than as a fault worth giving up over, and reloads once the build it is looking at is no longer the build it started with. ── Support The page was a decorated placeholder: a button that raised a toast saying the form was "only hinted at in the prototype", three invented ticket titles living in the translation file, and no way to see what became of anything. It looked thin because nothing on it was real. It now leads with the customer's own requests — what somebody arriving here wants to know is what they asked and whether anyone answered — with contact details moved to the side where they belong. The form attaches the plan, the instance and who is asking automatically: making a customer describe their own server back to the people who built it is the part of support people hate. FAQ answers end where the thing can actually be done, and "is it me or is it you" is answered by a link to the status page. 637 tests. R20 recorded in CLAUDE.md and enforced by EditInModalTest: no page view may grow an input field inside a <td>. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
3699c7cdb9 |
Ask the tunnel gateway where it actually listens
tests / pest (push) Successful in 7m15s
Details
tests / assets (push) Successful in 20s
Details
tests / release (push) Successful in 5s
Details
The console has been unreachable over the VPN, and the cause was the readiness probe rather than the tunnel. The gateway binds to the WireGuard hub address alone; the probe asked 127.0.0.1, where nothing has ever listened. It could only fail, so VPN_READY stayed false, and on that basis the application withheld the `DNS = 10.66.0.1` line from every client config it issued. A phone then built the tunnel, asked its normal resolver for the console hostname, got the public address and had its connection closed — indistinguishable from "this site does not exist", which is exactly what it looked like. Probing over TLS on the bare address would not have worked either: the site matches on the console's hostname, so a request without SNI is offered no certificate. The gateway now answers a plain-HTTP health port on the hub address, which removes TLS, SNI and name resolution from the question and answers only what is being asked — is this gateway listening, in this network namespace, right now. Caddy refuses to start when the certificate is unreadable, so a health port that answers still proves the whole file loaded. Also here, all found while looking: - Icons pushed their label onto a second line and rendered a size larger than asked for. Tailwind's preflight makes an svg display:block, and `.size-4` and `.size-5` have equal specificity, so stylesheet order decided — and it emits size-4 first. Every icon written as 16px was silently 20px. Recorded as R18. - Four error pages printed `errors.404.hint` in place of a sentence: the lang files give those codes a null hint and Laravel returns the key for a null line. - The Developer role had no label in either language, so the dropdown showed `admin_settings.role_developer`. - The secrets area held one key, which is not worth a password gate. It now carries the credentials that actually stop the business when they expire — DNS, monitoring, SMTP — and the test button appears only where a checker exists, instead of reporting on Stripe whatever was being looked at. - The update button said nothing about when a queued run would start or where a running one had got to; both are now shown, and a failure names its step. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
f6ddf45dc0 |
feat(admin): a console for creating, pricing and scheduling plans
The owner can now do from the console what only a config edit could do before: create a plan line, draft a version, price it, publish it into a window, and pull a plan out of the shop. Two pages and one modal, because the catalogue has exactly two levels. The plan list is a name, a rank and a kill switch. Everything that can be got wrong — capabilities, prices, windows — lives on the versions, where the page is built around the one rule that matters: a draft is freely editable, a published version is not touchable at all. So drafting and publishing are separate acts, and publishing says plainly that it is final. The console refuses everything the catalogue refuses, on the form rather than as a stack trace: an overlapping window, a window that ends before it starts, a version with no price for a term we sell on, and — new here — a version with no VM template, which would be bought and then fail provisioning every time. Numbers are bounded on both sides, because a mistyped price otherwise overflows the column and answers with a 500 instead of saying which field was wrong. Performance class and features are picked from the keys we have labels for; free text would be frozen at publication and shown to customers as a raw translation key forever. Concurrency, since two admins share one catalogue: draft numbers are allocated under a lock on the family, publication is an atomic conditional claim, and discarding a draft is one statement conditional on it still being a draft — otherwise a draft published in the meantime could be deleted out from under the customers now contracted to it. `plans.manage` (Owner and Admin) guards the pages themselves, not only the buttons, and the modal authorises itself — prices, drafts and unreleased plans are not something to leave readable to anyone who types the URL. Also fixes the shared checkbox, which ticked in the browser's own blue on every page that used one: a native checkbox ignores text colour and needs accent-color. Verified in the browser: withdrawing a plan in the console removes it from the customer's billing page immediately, and restoring it brings it back. The public landing page carries no catalogue-driven plan list, so there is nothing there to hide. 421 tests green. Codex review clean after six rounds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
556a560506 |
feat(vpn): explain a missing download, and offer a way out
An access created without storing its config had no download button and no explanation — it looked broken. It now shows a dimmed button that says why (the private key only exists on your device), and every staff access gets "Re-issue": a new keypair, old key off the hub before the new one goes on, so two peers never claim the same tunnel address at once. A stored config is re-encrypted in step, or the owner would download a key the hub no longer accepts. Host peers are excluded: their key belongs to the machine's own wg0, and swapping it here would cut the host off with nothing left to repair it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|
|
|
7f60f88542 |
fix(maintenance): make the window form usable
The action labels wrapped to two lines and crushed the column — now icon buttons with tooltips, like every other admin table. The form column was too narrow for its content: the datetime fields silently clipped their own value, so they are stacked and full width now, with duration chips because typing an end timestamp by hand is the fiddliest part of the form. Host rows show a real selected state and a per-datacenter count, and the impact column says "1 Host · 1 Kunde" instead of "Host(s) · Kunde(n)". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|
|
|
0d9b62eb50 |
feat(vpn): ownership, a Developer role, and password-gated config retrieval
Reworked after a design consultation with Codex, which pushed back on my first proposal in three useful ways. Ownership and rights: - vpn.manage split into vpn.view.all and vpn.manage.all. Seeing an access is not managing it, and neither is holding its private key. - Record-level rules live in VpnPeerPolicy, not in permissions: an access belongs to a person, and ownership is what grants sight of it. Every operator reaches the page, but sees only their own unless they may see all. - Issuing an access is not self-service — it reaches the management network, so it needs vpn.manage.all even for oneself. - New Developer role: sees everything, manages nothing. Writing code does not imply authority over other people's access. - kind (staff|host|system) replaces a null user_id that had to mean two different things at once. Config storage, opt-in per access: - Downloading is owner-only — explicitly NOT for view.all or manage.all. An admin who needs access revokes this one and issues their own, which keeps the record of who holds what honest. - The password is asked on EVERY retrieval, rate-limited. Laravel's password.confirm keeps a 15-minute session stamp, which would authorise unlimited later downloads from an unattended browser. - Stored under VPN_CONFIG_KEY, not APP_KEY: a leaked application key must not also hand over the management network. Purged on revocation and when a staff member is revoked — console taken away, tunnel left open was the worst case. - The plaintext never enters a Livewire snapshot (the page polls every five seconds); the component carries an opaque handle instead. Also: copy button works without HTTPS again (navigator.clipboard only exists in a secure context, so over http it silently did nothing), plus config download as a file and a QR code for the mobile apps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|
|
|
71ca0e1394 |
feat(admin): VPN access management with live peer state
The console can now create, block and remove VPN accesses, and shows who is actually connected — traffic counters, source endpoint and last handshake. Design notes: - The web container has no wg0, so the page never talks to WireGuard. Live state is copied in by SyncVpnPeers on the provisioning queue (the only worker whose container owns the interface); the page polls the database and merely nudges that job, throttled so many open tabs cannot flood the queue. - enabled (operator intent) and present (observed on the hub) are stored separately, so a sync can never quietly undo a block and drift stays visible as "wird angewendet". - The sync adopts peers the host pipeline registered, naming them after their host — otherwise "who is on the VPN" would have blind spots. - Keypairs are generated in PHP via libsodium rather than shelling out to wg genkey (no interface in this container, and it keeps generation testable). The private key is shown once and never stored. - allocateIp() now also considers vpn_peers, which would otherwise be handed a tunnel address a host already holds. - vpn.manage is a new capability held by Owner/Admin only, and it hides the nav entry too — a VPN access reaches the management network. Verified end to end against the real hub, not just mocks: created an access in the browser, connected with the generated config, watched the console flip to "Verbunden" with real counters, then blocked it and confirmed the peer was gone from wg0 and the client could no longer handshake. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|
|
|
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> |
|
|
|
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> |
|
|
|
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> |
|
|
|
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> |
|
|
|
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> |