phase-2-3-inventory-operations #2

Merged
Luxxy merged 6 commits from phase-2-3-inventory-operations into main 2026-08-02 12:15:15 +00:00
Owner
No description provided.
The three pieces the create-instance wizard sits on.

- domain/plans: one place decides what a plan permits, consulted by both the
  DTO the UI renders controls from and the handler that enforces it. A plan a
  customer does not have is distinguished from a limit they have used up,
  because only one of those is fixed by deleting something. An instance with
  no plan gets no optional features — a missing plan is not an unlimited one
- repositories/plans: retiring a plan withdraws it from sale without touching
  the instances already on it
- services/scheduler: least-loaded rather than first-fit, so one node is not
  packed full while its neighbour idles and takes a disproportionate share of
  customers down with it. Scores on the bottleneck rather than an average,
  because a node with spare CPU and no spare memory refuses the next instance
  for memory. Carries a per-node account of every rejection, which turns "no
  capacity" into something an operator can act on
- services/instances: every resource figure comes from the plan, never the
  request. The runtime name is derived from the instance id — opaque, unique
  without a lookup, and safe in a shell — so no customer-chosen string ever
  reaches a host as an identifier
- api-types/customer: the create request has no field for a node, a pool, a
  profile, a config map or a memory figure, and a test asserts a request
  carrying them anyway changes nothing

819 tests passing, clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A customer can now create a server: catalogue, keys, placement, dispatch.

- domain/ssh_keys: refuses private key material in every format it arrives in,
  checked before anything else so a disguised one cannot slip past, and
  reports it as requiring rotation rather than as a validation nit — by the
  time it reaches us it has crossed a network. Fingerprints are computed from
  the decoded blob, so the uniqueness constraint means "the same key" rather
  than "the same string"; the customer's comment is dropped rather than
  installed into every guest's authorized_keys
- repositories/ssh_keys: every function names an owner, with a test that walks
  the module's own surface and requires it. There is no find(id)
- api/customer/catalogue: plans, images and regions — three routes, counted by
  a test so a fourth has to be justified. Region availability is computed
  against the smallest enabled plan rather than stored, so a customer cannot
  complete a wizard into a region that will refuse them
- api/customer/instances: create validates, places, writes the row and the
  operation, then dispatches. SSH key ids resolve against the caller's own
  keys, so a stranger's key cannot be authorised by guessing an identifier
- repositories/instances: a new row starts at `creating`, never at the desired
  state — a green badge before a node has been contacted would be a lie

846 tests passing, clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- repositories/snapshots: the record is written at `creating` before the node
  is contacted, so a panel restart mid-flight leaves a record rather than a
  snapshot nothing knows about. A snapshot already being deleted does not
  count against the plan limit, or a customer at their ceiling would be stuck
  behind a delete that is already underway
- api/customer/snapshots: three gates — permission, project role, and what the
  plan includes — and the plan gate is the same function the DTO uses, so a
  visible button cannot produce a refusal. Runtime names are generated, since
  a customer-supplied snapshot name containing a slash addresses something
  else entirely on some runtimes
- repositories/metrics: every read is bucketed in the database, so a chart is
  roughly two hundred points whether it covers an hour or a month. A bucket
  with no samples is absent rather than zero — an instance that was stopped
  did not use 0% CPU. A counter reset reads as unknown rather than as negative
  transfer. Disk read and write stay null because the runtime reports usage,
  and a usage figure in a column named `disk_read` is a number an operator
  would read as something it is not
- api/customer/metrics: an unrecognised range is rejected rather than
  defaulted, so a chart cannot be labelled with the wrong period
- api/agents/websocket: node samples come from the heartbeat that is already
  arriving; a failure to record one is a gap in a chart, not a reason to mark
  a healthy node offline

874 tests passing, clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A root shell on a customer's server, reached through a browser — so every
property here is deliberate.

- services/console: tokens are single-use, expire in sixty seconds, are bound
  to one user and one instance, and only their hash is stored. Redemption is
  an UPDATE guarded on `redeemed_at IS NULL`, so two requests presenting the
  same token race and exactly one wins; a check-then-update would let both
  through. Unknown, expired, redeemed and closed all answer identically,
  because distinguishing them tells an attacker which guess was closest
- services/console_registry: bounded per-browser queues. A terminal producing
  faster than the browser reads loses frames rather than exhausting the
  panel's memory, and a full queue does not tear down a working session
- api/websocket/console: the panel sits in the middle so no browser reaches a
  node. Output is sent as binary, because forcing terminal bytes through UTF-8
  validation corrupts anything a full-screen program wrote. A hard lifetime
  sits above the idle timeout, for a session kept alive by a script
- api/customer/instances: the console endpoint applies three gates —
  permission, project role, plan — and audits the issue with the session id,
  never the token

Console contents are never persisted or logged. The schema has no column for
them, nothing here holds them after delivery, and a test walks every tracing
call in the proxy to make sure none carries a payload — verified by injecting
one and watching it fail.

900 tests passing, clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- commands/instance: snapshot create, restore, delete, list and instance
  metrics now execute. Console is deliberately absent from this path — it is a
  stream rather than a command with a result, and running it here would mean a
  CommandResult that never arrives until the session ends
- commands/console: per-session channels with bounded queues, dimensions
  clamped again on the node because a node does not trust the control plane's
  arithmetic any more than the reverse. Output larger than one frame is split
  rather than dropped, so a `cat` of a large file survives intact. The
  registry's Debug reports a count and nothing else
- commands/metrics: the node computes CPU utilisation, because turning a
  cumulative counter into a rate needs two readings and one clock — and the
  node is where both exist. A counter reset reads as unknown rather than as a
  negative spike, the first sample for an instance reports nothing rather than
  a misleading zero, and history is swept so the map does not accumulate an
  entry per instance that ever ran

926 tests passing, clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 4 frontend: create-instance wizard, console, metric charts, and six real bugs found by actually running the system
Some checks failed
Agent CI / Against a real Incus daemon (pull_request) Has been skipped
Agent CI / Test the agent (pull_request) Failing after 3s
Backend CI / Format, lint and test (pull_request) Failing after 2s
Security / Advisories, licences and sources (pull_request) Failing after 4s
Backend CI / Migrations apply to an empty database (pull_request) Failing after 1s
Security / Frontend dependency audit (pull_request) Failing after 37s
Security / No secrets or placeholder credentials committed (pull_request) Failing after 2s
Frontend CI / Typecheck, lint, test and build (pull_request) Failing after 1m45s
Agent CI / The workspace still compiles off Linux (pull_request) Has been cancelled
aef658bb8c
Added the last customer-facing pieces: a create-server wizard, an instance
detail page with a CPU chart and an xterm console panel, both lazy-loaded so
recharts and xterm ship only to the page that needs them.

The rest of this commit is what a live end-to-end run turned up. Stood up a
real PostgreSQL and ran the actual backend and frontend against it — every
prior verification in this branch had been cargo test and cargo check, never
the running system — and walked a customer through signing up, browsing the
catalogue, and creating a server. Six real, load-bearing bugs surfaced that no
amount of unit testing had caught, because none of them are visible from
inside a single function:

- auth/sessions: session creation bound five parameters for a six-column
  insert; `expires_at` was computed and then never bound, so every login
  failed with a Postgres protocol error before this was fixed
- auth/seed, main: `SEED_DEVELOPMENT_ADMIN` was parsed into config and
  documented in three places but never actually consulted — no code path
  created the account it promised. Implemented `seed_development_admin`,
  idempotent like the existing role seeding, and wired it into startup
- crates/core/permissions: the customer role's default permissions still
  carried a Phase-1 comment reasoning that self-service provisioning "is a
  later capability" — it no longer was, but the grant was never added, so the
  create-instance wizard this phase built was unreachable by any real customer
- crates/api-types/pagination: `#[serde(flatten)] page: PageRequest` inside
  every list handler's query struct fails to deserialize `page`/`per_page`
  through axum's real query-string extractor — a known incompatibility
  between `flatten` and numeric fields that a `serde_json`-based unit test
  cannot see, because JSON isn't self-describing-string the way a query
  string is. Every paginated endpoint in the app returned 400 the moment a
  browser sent `?page=1`, which is every request a browser sends. Fixed with
  a custom deserializer that goes through an untagged enum (which requests
  `deserialize_any` rather than a specific type) and added a regression test
  using the actual `serde_urlencoded` crate axum depends on, not a stand-in
- components/ui/button: `Button asChild` rendered a conditional spinner as a
  JSX sibling of `children`; even when the condition was false this produced
  a two-element children array, and Radix `Slot` throws on anything but
  exactly one. Affected every `asChild` button in the app, not just the one
  that happened to surface it first
- api/customer/instances: `capabilities_of` still hardcoded
  console/snapshots/backups/reinstall to `false` from before Phase 4's plan
  system existed, so the console and snapshot UI built in this phase was
  wired up and unreachable — no plan, however generous, could turn a
  capability on. Now consults `Plan::capabilities`, the same function the
  snapshot and console handlers themselves check before acting
- api/admin/dashboard: `active_operations` and `failed_operations_24h` were
  hardcoded to `0` behind a comment claiming operations hadn't landed yet,
  which stopped being true in Phase 3. Wired to the `status_counts` query
  built for exactly this

938 tests passing, clippy clean. Verified live: login, dev-admin and
project-scoped customer accounts, the full catalogue-to-create-instance path
through the real scheduler (including a clean refusal when no node has
capacity, and a clean operation failure when a node has no live agent),
power-action state gating, and every admin and customer page rendering real
data end to end in a browser.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Luxxy merged commit b269b3662b into main 2026-08-02 12:15:15 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Luxxy/beegoon!2
No description provided.