8 Commits (b3651a14be91cddb8e9352c223d0079880d475a5)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
0671e8e119 |
Show one design while updating, not two swapping mid-run
Reported from live: pressing update showed a blue panel first, which then vanished and was replaced by a completely different "we are updating" screen. Both were mine. The console drew its own overlay in the app's light tokens; a moment later the containers went down and Laravel's 503 page took over with its own standalone styling. One event, two faces, swapping while an operator watches. There is now one panel — resources/views/partials/updating-panel.blade.php — rendered by both. It carries its own inline CSS and cpu- prefixed class names because the 503 page is served with the application down: no compiled stylesheet, no Tailwind, nothing but what is in that file. Dark mode comes from prefers-color-scheme for the same reason. The operator detail (step, running since, log tail) rides inside the panel behind a flag, since the same file is shown to customers on the 503 page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
20329b176b |
Refuse a portal login for an address that already belongs to an operator
Customer::ensureUser() enforced uniqueness only within users, so a customer provisioned (or registering publicly) with an operator's email got a second, colliding identity for the same address — the exact thing R21 exists to prevent. Checks Operator by address now, not by session (the deleted assertNotAdmin() checked who was signed in, which could never be the right question). Public registration gets the same guard via a plain unique() validation rule, reusing Laravel's stock "already taken" message rather than a dedicated one — naming the collision specifically would tell a public visitor an address belongs to staff, a worse leak than the generic refusal every other collision here already gives them. Impersonation answers 409 instead of crashing into it. The Stripe webhook keeps recording the paid order (same principle as openContract()'s own comment: the order is proof money changed hands) and withholds only the colliding login, logged for an operator to resolve by hand. Verified the reverse direction Codex flagged as already covered: two Admin\Settings actions and the clupilot:create-operator command all already refuse an operator email that collides with a customer's, and that check is sound today because every users row is created in lockstep with a customers row of the same email (CreateNewUser and ensureUser are the only two creation sites, both confirmed by grep) and nothing in this app can change a user's email afterwards (Fortify's updateProfileInformation feature is disabled, and no other code calls that action). |
|
|
|
6376f5e9fa |
One design system for every surface
The console was a second product. admin-tokens.css held a complete dark set — its own surfaces, its own orange, its own status triad — so every shared component had to work in two worlds, and the two halves of the application drifted apart in ways nobody could see from inside either one. That file now carries density and nothing else: smaller type, tighter rows, tighter corners. Colour comes from one place. The tokens themselves are rebuilt around what the redesign settled on: - Radii scaled to object size (9/11/16/22) instead of one value everywhere, which is what made the interface read as a design-system specification rather than as a product. - Warm, broad, low shadows. A neutral grey drop shadow belongs to a different design language and reads as cold on this ground. - --accent-press, which several rules already referenced and nothing defined. An undefined var() does not make a browser skip the declaration; it computes to , and for background-color that is transparent — the accent button lost its fill on hover and left white text on white. - No serif. A serif headline is what made every page read as a document, which is the impression the whole redesign exists to remove. --font-serif now points at the sans so nothing breaks while call sites are cleaned up. The two shells are one shell with two configurations, and both finally have a mobile navigation: the sidebar used to simply disappear below 900px, leaving no way to move around the application at all on a phone. The customer dashboard is bound to real records — instance, seats, traffic, backups, contract — instead of the fixtures it shipped with. Where something is not measured, storage consumption, it says what was contractually agreed rather than inventing a figure. This is the sheet a customer forwards to their auditor. 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> |
|
|
|
99715989aa |
Dress the placeholder page, and record why the obvious VPN fix cannot work
tests / pest (push) Successful in 8m55s
Details
tests / assets (push) Successful in 20s
Details
tests / release (push) Successful in 4s
Details
The page shown while the site is hidden was still in the old style — a coloured square and system fonts — on the two hostnames a visitor is most likely to try. It now uses the site's own plate, typeface and registration marks, still entirely self-contained because it is shown precisely when the asset build may not exist. Both it and the error pages lighten the accent in dark mode: the tone is chosen for contrast against paper, and on the ink plate the small text is the first thing to stop being readable. The VPN investigation ended somewhere useful, and not where it started. The blank page on the phone is the reverse proxy refusing the request — `respond 404` sends no body. It refuses because the phone is not on the tunnel: the client config lists only the management subnet in AllowedIPs, so a request to the console's public hostname goes out over the mobile network and arrives from a carrier address. It also explains "last contact: never", since WireGuard only performs a handshake when it has traffic to send, and nothing is ever routed into the tunnel. The obvious fix — add the server's public address to AllowedIPs — is written down here as a comment and a test rather than as code, because it cannot work. The WireGuard endpoint is that same address, so routing it into the tunnel routes the handshake packets into the tunnel they are trying to establish. The result is a loop and a connection that never comes up: the same blank page, now with no way in at all. Reaching the console over the VPN needs an address INSIDE the subnet — the proxy answering on the hub address, and a name that resolves to it. That is a deployment change, not something a config line can express. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
bc8bbc56a5 |
Make the console's access list reach the proxy, price in euros, and answer errors
tests / pest (push) Successful in 7m4s
Details
tests / assets (push) Successful in 19s
Details
tests / release (push) Successful in 4s
Details
The console was unreachable, and not for the reason it looked like. Two causes, both mine. A failed `artisan optimize` — the one that hit duplicate route names — left a broken route cache behind, so every request to the console host answered 500. Rebuilt. Underneath that: the reverse proxy has its own console allowlist, hard-coded, and it runs BEFORE the application. Everything added on the console's own access page was therefore ineffective, and when the owner's address changed they were turned away by the proxy before reaching the page that would have fixed it. The proxy now imports a fragment generated from the same list the console manages, regenerated by the host agent and reloaded when it changes. Getting that safe took most of the review. It refuses to rewrite an ambiguous Caddyfile rather than replacing some other site's matcher; it never falls back to a loopback-only list when the application cannot be reached, because that list validates cleanly and locks out every remote operator; it retries a reload that failed instead of assuming it worked; and an installation that upgrades without rerunning the installer is told, because otherwise the whole mechanism is invisibly absent. Prices are entered in euros. The form asked for cents, so €799 was typed as 79900 and one slipped digit was a factor of ten on an invoice. Conversion happens in one place, on the string rather than through a float — (float) '79.90' × 100 is 7989.999… and casting truncates to 7989, one cent short on exactly the prices people charge — and it refuses an amount the column cannot hold instead of failing at the database. Every error code has a page now, in the site's own language and typeface, self-contained so it still renders when the asset manifest is the thing that broke. There was only a bare white 404. The VPN list stops being a seven-column table nobody could fit: names broke across two lines and so did the headings. These are attributes of one access, not quantities compared down a column, so each access is a row — identity on one line, measurements in mono on the next. The status page says what it measures rather than what it promises. "New orders are delivered without failures" read as a marketing claim on a page whose only job is to be believed, and said nothing about the last 24 hours. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
2c0884357c |
fix(pages): the update and placeholder pages must not need a build
tests / pest (push) Failing after 7m49s
Details
tests / assets (push) Successful in 26s
Details
tests / release (push) Has been skipped
Details
CI caught it: both rendered through @vite, so a missing manifest threw a view exception instead of a page. That is not a test problem — the update page is shown precisely while the application is mid-deploy or freshly installed, which is exactly when the manifest may be absent. Both carry their own small stylesheet now and render with nothing built at all. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|
|
|
8117630a65 |
feat(ops): update page, automatic 419 recovery, CI workflow
Three things that bit us on every deploy: - artisan down now renders a branded page that says an update is running and reloads itself, instead of Laravel's bare 503. - A page left open across a deploy carries a Livewire snapshot and CSRF token the new code rejects — the user got "419 Page Expired" and a dead interface. A 419 from a /livewire/ request now reloads the page; the session is still valid, so that is all it takes. Hooked at the fetch layer rather than through Livewire's request hook, whose failure callback is not invoked for this case in the installed version — verified against a real 419 in the browser, not a simulated one. - Vite no longer empties its output directory: wiping it is what left an open page without its stylesheet mid-deploy, which is the "design is completely broken" symptom. update.sh also rebuilds the caches with optimize:clear + optimize instead of leaving them half-warm. Plus CI: .gitea/workflows/tests.yml (Pest + asset build, and a tested- tag only on a green main) and an opt-in act_runner service under the ci profile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |