138 Commits
Author SHA1 Message Date
mrhid6 9d17f539b5 fix: Fixed card header margin
Chart Release / chart (push) Successful in 19s
Server Deploy / deploy (push) Successful in 1m0s
2026-09-07 08:36:18 +00:00
mrhid6 9d218cb19f feat: Reduce server vitials panel height
Chart Release / chart (push) Successful in 18s
Server Deploy / deploy (push) Successful in 1m10s
2026-09-07 08:14:47 +00:00
mrhid6 f9ef9c4929 fix: Fixed mobile scroll bar
Chart Release / chart (push) Successful in 25s
Server Deploy / deploy (push) Successful in 2m17s
2026-09-07 08:02:09 +00:00
mrhid6 049e005873 feat: Updated monitor chart
Chart Release / chart (push) Successful in 15s
Server Deploy / deploy (push) Successful in 1m9s
2026-08-25 15:02:04 +00:00
mrhid6 c440b59b93 feat: Updated affected components on status page incidents
Chart Release / chart (push) Successful in 13s
Server Deploy / deploy (push) Canceled after 6m59s
2026-08-25 14:56:50 +00:00
mrhid6 270d55e6a6 feat: Updated status page title 2026-08-25 14:11:21 +00:00
mrhid6 7a0a1953f6 fix: Fixed api url on web
Chart Release / chart (push) Successful in 10s
Server Deploy / deploy (push) Successful in 7m37s
2026-08-25 13:53:59 +00:00
mrhid6 3e4ccc9720 feat: Added more debug logging
Chart Release / chart (push) Successful in 16s
Server Deploy / deploy (push) Successful in 7m1s
2026-08-25 13:26:23 +00:00
mrhid6 e5947489e4 fix: Fixed status page published switch 2026-08-25 13:26:10 +00:00
mrhid6 0a7a10aeed feat: Added status pages to license page 2026-08-25 13:10:28 +00:00
mrhid6 32e7420d89 fix: wire status page and incident delete, stop promising a name we do not publish
- The display-name placeholder showed the monitor's own name, reading as "leave
  this blank and we will use it". The server deliberately does the opposite: a
  blank `display_name` publishes the raw monitor id, because publishing an
  internal name has to be a decision. The placeholder now says "Public name
  (required)" and Save is refused until every component has one, so nobody adds
  five monitors and discovers five UUIDs on their public page. The server
  fallback is unchanged.
- `deleteStatusPage` and `deleteStatusIncident` existed in the api client and
  were wired to nothing, and the page address is immutable — delete was the
  only correction for a typo and there was no way to reach it. The editor
  header gains a typed-confirmation Delete page, and each incident row a
  confirmed delete, both on the existing ConfirmDialog.
- The create modal's address hint had lost its em dash and read as a broken
  sentence.
2026-08-25 09:05:06 +00:00
mrhid6 da1dc90ac5 fix: resolve the public status page's tenant from a trusted X-Forwarded-Host
The SSR fetch set `Host` to the visitor's hostname. `Host` is a forbidden
header name and undici discards it silently, so the Go server saw
`server:8080`, `hostSlug` returned "", `InstanceFromHost` returned false and
every public status page 404'd on every deployment. The feature did not work.

- `web/` now forwards the visitor's host as `X-Forwarded-Host`, and their
  address on `X-Forwarded-For` — without the latter gin sees a request from the
  Next pod with no XFF and every visitor of every page shares one 120/min
  bucket, tripped by exactly the traffic an outage produces.
- `publicStatusInstance` honours `X-Forwarded-Host` only when `c.RemoteIP()` is
  in `TRUSTED_PROXIES`. It is a tenant selector, so an untrusted peer must not
  be able to name one; `RemoteIP()` rather than `ClientIP()` because the latter
  is reconstructed from the very headers being judged. `TrustedProxies()` moves
  from main.go into the api package so the variable keeps one parser.
- A host naming no slug on a non-cloud deployment resolves the sole instance,
  the way bootstrap does. A self-hosted install at vantage.acme.com or an IP
  has no slug and could never serve a status page; more than one instance is a
  404 rather than a guess, and an unknown-but-well-formed slug stays a 404.
- `InstanceFromHost` gains an explicit-host variant rather than a second copy
  of the slug rules, and now caches negative lookups: an unknown host cost a
  Mongo query per anonymous request, which is also a timing oracle separating
  "no such instance" from "instance exists, page does not".
- The handler's `@Router` annotation is dropped. openapi.json declares one
  server of `/api`, so it published `/api/public/status/{pageId}` — a path that
  does not exist. The real address is described in prose instead.
2026-08-25 09:04:47 +00:00
mrhid6 fa67d839cd fix: status page editor error handling, maintenance validation and UTC display 2026-08-25 08:40:05 +00:00
mrhid6 1452928b75 feat: status page authoring UI 2026-08-25 08:32:43 +00:00
mrhid6 f4f41e400b feat: public status page 2026-08-24 14:53:42 +00:00
mrhid6 22b99ff895 feat: Monitor grath zoom
Chart Release / chart (push) Successful in 13s
Server Deploy / deploy (push) Successful in 6m31s
2026-08-24 10:58:50 +00:00
mrhid6 2fab784ba7 feat: Monitor groups and chart information
Chart Release / chart (push) Successful in 15s
Server Deploy / deploy (push) Successful in 6m35s
2026-08-24 10:30:23 +00:00
mrhid6 aa1c8e4aa1 feat: Hide secrets on api and channels
Chart Release / chart (push) Successful in 15s
Server Deploy / deploy (push) Successful in 8m5s
2026-08-14 12:23:36 +00:00
mrhid6 a0fbf5b9ba fix: Word and colour Windows workloads correctly across the UI
Server Deploy / deploy (push) Canceled after 0s
Chart Release / chart (push) Canceled after 0s
Agent Release / build (push) Successful in 14m1s
Agent Release / msi (push) Successful in 1m22s
2026-08-13 12:57:37 +00:00
mrhid6 36848a519a feat: Word the workload and update panels for Windows servers 2026-08-13 12:16:10 +00:00
mrhid6 7e767ecb4f fix: Move the API keys page off the /api prefix
/api-keys shares a raw string prefix with /api, and the proxies in front
of this app do not all match by path segment. Nginx Proxy Manager routes
/api straight to the Go server with a prefix location, so /api-keys never
reached Next at all — it reached a control plane with no such route and
came back as a JSON 404. Traefik's PathPrefix has the same shape of
matcher, which puts the Helm ingress at risk whenever ingress.api.enabled
is on.

The page is /tokens now, which cannot collide with anything, and which
matches the /api/tokens the REST API already publishes. The sidebar still
says API Keys — the label is for the reader, the path is for the router.

A permanent redirect covers anyone who bookmarked the old path today.
Fixing the proxy config instead would have left the trap set for the next
deployment, and for whatever sits in front of it.
2026-08-13 09:26:34 +00:00
mrhid6 689d0e1d5b feat: Give API keys their own page and group the sidebar
The token management card sat on /settings, which is owner|admin
throughout, so it hid a capability every member already had: the API has
never required a role to mint or revoke your own key. It is now the
/api-keys page, reachable at every role, with the instance-wide lifetime
cap left behind on /settings because that is policy rather than one
person's credentials — and that split is what lets the page be ungated.

The sidebar gains groups: Fleet, Access, Automation, Instance, each with a
small-caps heading and a rule above it. Grouping is by what the operator
is doing rather than by which service answers, so SSH keys, secrets and
API keys sit together as credentials. A group whose every item is
admin-only disappears whole for a member; a labelled section with nothing
under it reads as a failure rather than a restriction.

The UI says keys while the collection, prefix and routes still say tokens.
Renaming a published endpoint to match a nav label would break every
script already written against it.
2026-08-13 08:54:42 +00:00
mrhid6 182752d9ab feat: Manage API tokens from settings
A card in the Access group beside Members and single sign-on rather than a
new nav entry — /settings/instance was folded back in for exactly this
reason. The plaintext is shown once in a well block and never again.

Tokens outside a newly tightened lifetime policy are flagged rather than
broken, because the policy governs issuance, not existing credentials.
2026-08-12 14:58:57 +00:00
mrhid6 675689a458 feat(audit): server-side paging, search and category filter; one event format
Chart Release / chart (push) Successful in 12s
Server Deploy / deploy (push) Successful in 8m2s
The page rendered a map of eleven event types to labels and seven to colours.
The server emits forty-seven. Everything unmapped fell through to its raw
string, so "Key Assigned" in green sat above "workflow.schedule_updated" in
grey — the same kind of fact in two formats, which made the column look like it
carried a meaning it did not.

Presentation is now derived rather than enumerated. Event types are named
<category>.<action> by every call site, so the category becomes a chip, the
action is humanised, and the tone comes from the verb. A type added to the
server tomorrow gets a sensible label and colour with no second list to update;
the override table holds only the dozen the rule reads badly for. Every row is
one treatment, and colour never carries meaning alone — the sentence beside it
says the same thing in words.

Paging and filtering are server-side, unlike the fleet lists that answer with
everything and slice in the browser. audit_retention_days is a licensed
entitlement measured in months, and this log is read to answer questions about
the past, so a browser filtering the most recent page would report "no results"
for events that exist. GET /api/audit now takes q, category, limit and skip and
answers {events, total} — a short page is not evidence of the end of the log,
which is why the total is counted rather than inferred.

audit_logs had no indexes at all: every read was a collection scan with an
in-memory sort over an append-only collection. Adds (instance_id, created_at)
and warns rather than failing, matching EnsureSecretIndexes.

Two bugs found by running the deriver over all forty-seven real types rather
than eyeballing it: the tone rules matched only past-tense verbs, leaving
auth_provider.delete drawn as neutral beside key.deleted in red; and
"unaccepted" matched "accepted", so withdrawing an acceptance read as the same
caution as granting one.
2026-08-10 15:25:48 +01:00
mrhid6 42f3f3e640 feat(web): typed confirmation for deleting an SSH key
Chart Release / chart (push) Successful in 14s
Server Deploy / deploy (push) Successful in 1m37s
The last of the inline two-step deletes, and the one with the most reach: a
key delete revokes it from every server at once, and for a generated key the
stored private half goes with it. That copy is the only one Vantage holds, so
unlike an uploaded key this cannot be undone by pasting the public half back.

Body names how many servers lose access, and says the private key is destroyed
only when there is one to destroy.

The delete error moves out of the toast and into the dialog, which stays open
on failure, matching the server and monitor deletes.
2026-08-10 15:07:52 +01:00
mrhid6 21238fe707 feat(web): typed confirmations on destructive actions; toasts for API outcomes
Chart Release / chart (push) Successful in 22s
Server Deploy / deploy (push) Successful in 48s
Destructive actions were confirmed by a second danger button rendered where
the first one had been, so a double click on Remove deleted the thing without
the operator ever reading which thing it was. Servers, monitors, secret keys
and sign-in providers now go through ConfirmDialog with requireTyped, matching
the secret-group delete that already worked this way. Notification channels and
vulnerability alert rules get an untyped dialog: both are a name and a URL and
are rebuilt in a minute, but neither had any confirmation at all, and both
silently stop alerts that nobody misses until an incident goes unannounced.

Mutations otherwise succeeded in silence, or reported into whatever inline
banner the page happened to own. Two failure modes came of that: a modal that
closed on error left the message nowhere to land, and a save that was rejected
left the old values on screen looking exactly like a save that worked
(/settings had no error path at all). Every mutation now reports through the
existing toast context.

Errors stay inline where the surface that raised them is still on screen and
the message is a correction to make in it: form validation, the cron field,
the tag rows, a rejected licence blob, and the workflow designer's autosave,
which is a standing condition rather than an event. Everything else toasts.

Ad-hoc feedback removed in favour of it: the "Sent!" button labels on the
server maintenance tab, the settings "Saved!" flag, the channel test line, and
the steps page's notice/error pair.
2026-08-10 14:26:02 +01:00
mrhid6 e434beec7a fix(web): toasts survive an open dialog; empty-state actions can be gated
Both from the final review, and the first one overturns a call I got wrong.

The aria-hidden sweep that makes aria-modal true also swallowed the toasts.
ToastProvider renders inside the app root, and every modal-raised confirmation
is toasted *before* its dialog closes — "Saved …", "Deleted …", "Removed …" —
so each one was inserted into a hidden subtree and never announced. Un-hiding
a live region afterwards does not replay what it missed. The toast layer is
portalled to the body carrying the dialog-layer attribute, which exempts it
from the sweep, and sits above the dialog: a toast explaining why a dialog's
action failed is no use behind it.

EmptyState's action was narrower than the call site it replaced. The old
first-workflow button carried loading={isPending}; the new one carried
nothing, so a double click created two workflows. The action is a union now —
a link takes no pending state, a handler takes loading and disabled.
2026-08-10 10:01:47 +01:00
mrhid6 fe1dfe472a refactor(web): list pages share the async primitives; dialogs hide the page behind
Keys, secrets, audit, steps and workflows each carried the same
loading/error/empty ternary with its own copy of the spinner and its own
wording for a failed fetch — five of them had drifted to five different
sentences for "the request did not come back". They go through AsyncBoundary
now, which means a skeleton in place of a spinner, a retry button on failure,
and backend messages passed through friendlyMessage rather than printed raw.

Steps gets the filtered-empty state the fleet just got: "no steps match that
filter" is not "no steps yet", and only one of them should offer to create the
first one.

Two more from review:

Modal's effect ran before `mounted` flipped, so a dialog rendered already open
found null refs and took no focus at all. It depends on `mounted` now.

aria-modal was a claim with no mechanism behind it — portalled to the body,
the app tree is a sibling of the dialog and a screen reader's virtual cursor
still browsed the page underneath. The body's other children are marked
aria-hidden while any dialog is open, refcounted alongside the scroll lock.
This does mean a toast raised while a dialog is open is not announced, which
is the correct trade for a modal: ConfirmDialog shows its own errors inline.
2026-08-10 09:54:32 +01:00
mrhid6 0cfaf6670c fix(web): address review of the dialog, toast and async primitives
Two of these were real defects in the previous two commits.

friendlyMessage discarded exactly the messages it claimed to keep: the
"is this a bare reason phrase" test was a shape regex, and "Default steps
cannot be edited" has the same shape as "Not Found". It is an exact-match set
of reason phrases now.

Modal depended on onKeyDown, which is rebuilt whenever onClose changes
identity — and onClose is an inline arrow at every call site, so any parent
re-render (a 30s poll, a mutation flipping to pending) tore the effect down
and rebuilt it: cleanup restored focus to the trigger, setup then moved it to
the top of the dialog, mid-typing. onClose is held in a ref and the effect is
keyed on `open` alone.

Also in Modal: initial focus takes the first control in the body rather than
the panel, since the header comes first in DOM order and every dialog was
opening on its own dismiss button; the Tab trap pulls focus back when it has
escaped the panel entirely rather than only handling the two ends; the scroll
lock is refcounted, because per-instance save/restore released the page when
an outer dialog unmounted under an open inner one; and the whole thing is
portalled to the body so a nested confirm is not clipped by its parent's
overflow box.

The fleet's filtered-empty state keyed on the search alone, so a tag filter
matching nothing told a customer with a full fleet to add their first server.

Remaining: pending mutation errors are reset when a confirm dialog closes, so
one member's failure no longer greets the next; ConfirmDialog clears typed
confirmation when the target changes, not only when it reopens; deleting a
workflow closes its dialogs before navigating rather than carrying a scroll
lock onto the next page; toasts split into a polite and an assertive region,
since one polite wrapper demotes the role="alert" children inside it; and a
custom skeleton gets a live "Loading" beside it, having been aria-hidden with
nothing else to announce.
2026-08-10 09:44:29 +01:00
mrhid6 4d67341ba5 feat(web): fleet search and sort, shared empty/error states, no background polling
The fleet list had a tag filter and nothing else: no search, no sort, and an
unbounded list. Searching hostname/address/OS and sorting by hostname, status
or last seen are all client-side, since the browser already holds the fleet
the page just fetched. Sorting by status orders by how much attention each
state wants rather than alphabetically, which is the only reason to sort by it.

The filtered count is shown beside the total so a search does not read as the
fleet having shrunk, and "no results" is a distinct empty state from "no
servers", with a way back out of the search.

refetchIntervalInBackground defaults to false on the query client. Polling
pages kept refetching in a hidden tab — the fleet list pulls inventory blobs
every 30s — so a console left open in a background tab polled until its
session expired. It belongs in the defaults because the argument is identical
on every polling page.
2026-08-10 09:35:39 +01:00
mrhid6 1fa9160c59 fix(web,adminsite): accessible dialogs, real confirmations, shared async UI
Four correctness/accessibility defects and the destructive-action flow.

- Button: the loading spinner carried xmlns="http://www.w3.instance/2000/svg",
  a find/replace of "org" that landed inside a URL. Button also grows an href
  form, because <Link><Button> nested a button inside an anchor at nineteen
  call sites: invalid markup, two tab stops, and Enter firing only the anchor.

- Fleet status was four meanings carried by hue with the distinction living in
  a title attribute, which touch never shows and screen readers need not
  announce. It now carries a text label and an accessible name, which is the
  one rule the design system states outright.

- Modal had no focus management at all: no trap, no initial focus, no restore,
  no scroll lock, no aria-labelledby. Dialogs nest (a confirm over an edit), so
  a stack decides which panel owns Escape and Tab.

- Seven destructive actions went through window.confirm(). ConfirmDialog
  replaces them and can say what is about to happen; deleting a secret group,
  a shared base step or a workflow now requires typing the name, since those
  have no undo and a wide blast radius. adminsite keeps its own inline idiom
  rather than importing a dialog system it does not have.

Adds Toast, AsyncBoundary/EmptyState/ErrorState/TableSkeleton and
friendlyMessage, replacing per-page loading ternaries and raw
(error as Error).message text. Wired here only where a call site was already
being edited; the remaining pages follow.
2026-08-10 09:25:53 +01:00
mrhid6 d559cccd44 feat: Updated server page
Chart Release / chart (push) Successful in 22s
Server Deploy / deploy (push) Successful in 45s
2026-08-07 16:21:17 +01:00
mrhid6 e28238191d feat: Added vuln filter
Chart Release / chart (push) Successful in 25s
Server Deploy / deploy (push) Successful in 2m29s
2026-08-07 11:58:42 +01:00
mrhid6 1993802c38 feat: Updated rescan button text 2026-08-07 10:52:16 +01:00
mrhid6 0c21765da3 feat: Added pagination
Chart Release / chart (push) Successful in 13s
Server Deploy / deploy (push) Successful in 1m39s
2026-08-07 09:59:50 +01:00
mrhid6 483053b9a2 feat: workload registry UI 2026-08-07 09:06:36 +01:00
mrhid6 d1769fc886 feat: Updated vuln style
Chart Release / chart (push) Successful in 12s
Server Deploy / deploy (push) Successful in 1m36s
2026-08-06 16:35:04 +01:00
mrhid6 81248bb159 style: bring the vulnerabilities page onto the house page shape
Every other page under app/(app) opens with `p-4 sm:p-6 lg:p-8` and the
layout adds none of its own, so this page alone sat flush against the shell
edge. Its h1 was text-xl where every other page is text-2xl.

The findings list was a stack of separately bordered cards; it is now rows
inside one Card, separated by border-border-soft, matching the monitors and
workflows lists. Loading is the shared spinner rather than a line of text,
the error is the shared danger strip, and the empty state uses the same
proportions as the monitors one.
2026-08-06 15:49:39 +01:00
mrhid6 9ba3d4a61f feat: Vulnerability Scanning feature on license page
Chart Release / chart (push) Successful in 12s
Server Deploy / deploy (push) Successful in 1m20s
2026-08-06 15:40:13 +01:00
mrhid6 84dfcfeac7 feat: vulnerability findings UI
Fleet board grouped by CVE, a per-server section on server detail, and
alert rules beside the channels they consume.

The server detail page has no tab pattern despite the plan saying to
follow one, so this adds a section in the existing vertical stack.

Three states are kept visually distinct because they are identical if
handled carelessly and only one is good news: never reported, no advisory
feed for the distribution, and scanned-and-clean. Database freshness sits
with the findings rather than in settings for the same reason.
2026-08-06 14:40:37 +01:00
mrhid6 5bba54f3e5 fix: Fixed style layout on workflow run page 2026-08-06 10:50:31 +01:00
mrhid6 85a8865892 feat: restyle the steps table and add 22 default steps
Chart Release / chart (push) Successful in 11s
Server Deploy / deploy (push) Successful in 2m46s
2026-08-04 17:34:18 +01:00
mrhid6 50a9ac5fdc fix: count tag-matched servers in the workflows list 2026-08-04 17:28:01 +01:00
mrhid6 3388d2f895 fix: Fixed padding on add step button
Chart Release / chart (push) Successful in 22s
Server Deploy / deploy (push) Successful in 41s
2026-08-04 17:24:40 +01:00
mrhid6 3a77fc2abd feat: edit target servers and tags together in the workflow modal 2026-08-04 17:21:12 +01:00
mrhid6 c2635ed51a fix: Fixed schedule workflow col
Chart Release / chart (push) Successful in 20s
Server Deploy / deploy (push) Successful in 41s
2026-08-04 14:42:10 +01:00
mrhid6 b21ac05547 feat: show workflow schedules in the list
Chart Release / chart (push) Successful in 24s
Server Deploy / deploy (push) Successful in 2m34s
2026-08-04 14:16:13 +01:00
mrhid6 2de7ac116b feat: filter the fleet by tag and target workflows by tag selector 2026-08-04 13:42:47 +01:00
mrhid6 fa1fd14ed1 feat: view and edit server tags 2026-08-04 13:38:42 +01:00
mrhid6 80f0afb28b feat: Updated monitors pages
Chart Release / chart (push) Successful in 13s
Server Deploy / deploy (push) Successful in 1m25s
2026-08-04 12:19:15 +01:00