7 Commits (feat/mailboxes)
| 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> |
|
|
|
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> |
|
|
|
85fcc154b8 |
Measure availability, and let a customer move down again
Availability is now counted from our own monitoring answers. Uptime Kuma knows the figure, but the REST bridge in front of it only answers "healthy, yes or no", and waiting for the bridge to grow an endpoint would have left the panel without the number indefinitely. The sync already asks on a schedule; counting the answers gives a percentage that is ours and explainable. An unanswered check is not a failed one — the counter only moves when a real answer arrived, so our monitoring going down does not become the customer's outage on the one figure they are asked to trust. An instance nobody has ever checked shows no availability rather than 100 %. The downgrade path existed nowhere. Going down is not the mirror of going up: it can ask an instance to hold more than the target plan allows. Smaller plans are now listed even when they cannot be taken, with the obstacle in the customer's own numbers — "you have 31 users, this package allows 10" — because a greyed-out button that does not name what is in the way is the thing people ring about. Storage is checked against the last real reading rather than the contractual allowance, or anyone who once bought a large plan could never leave it. The limit is re-checked in the action: a rule enforced only in markup is not enforced. And the smaller corrections from this round: - The cloud tab carried a hardcoded "B" as the instance's initial, and the PHP and MariaDB versions, which tell a tenant nothing they can act on. - "EU — Serverstandort im Angebot festgehalten" under a label already reading "Serverstandort" was two sentences saying nothing. It is the country. - Support promised a reply "binnen 4 Std." with nothing behind it. What is true everywhere else is an answer on the same working day. - The actions column appeared even when no row in the table had an action. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
676c38643c |
Follow the approved template, and stop showing invented records
The dashboard was built to my own idea of the layout rather than to the template that was signed off: a coloured pill where the template has a stamped mono line, bare figures where it has a label with a chevron, a unit on its own line instead of beside the number, and no ring at all. There is no point agreeing a design and then building something adjacent to it. The metric card is now a component that draws the template's form, and the pages use it. Two more places were still showing data that does not exist: - Support listed three tickets to every customer, complete with reference numbers and dates. There is no ticket model; it was fixture text. A request list also belongs in the console, where an operator can see who filed what. Removed until there is something real behind it. - The datacentre name reached the customer through the cloud tab's copy as well. Customer surfaces name the jurisdiction; the building is operator information. And the actions column rendered an empty cell for the owner, who can be neither re-invited nor revoked. An empty cell reads as a missing feature; it now says so with a dash and a title. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
a321d963d7 |
fix(portal): no silent no-ops without a customer; maintenance visible per instance; maintenance form redesign
- ResolvesCustomer trait replaces 5 duplicated customer() lookups; portal actions now SAY why nothing happened (operator accounts have no Customer) instead of returning silently — the actual cause of 'dead buttons', '0/0 seats' and 'settings not saving' when signed in as admin - portal layout: explicit notice for a login without a linked customer - /cloud: per-instance maintenance badge + window details; seat note 'owner = seat 1' - maintenance form: sectioned card, hints, placeholder, styled datetime fields, grouped host picker with per-datacenter select-all + selection counter Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|
|
|
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> |
|
|
|
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> |