Added
-
Map identity-provider groups to roles from the app. Which SSO group
grants which role on which projects was a chart value —auth.oidc.mapping
invalues.yaml, onehelm upgradeper change — so giving a team access
meant a deploy. Settings → Access now shows those rules and lets an admin
add, edit and delete rules of their own beside them. The chart stays the
declared base: its rules render read-only, and on a name collision the chart
wins — an authored rule for a group the chart also declares is kept and
marked as overridden, with the reason on its row, instead of being silently
ignored or refused outright. A change applies to that group's next sign-in
or token refresh and reaches every hub replica within about fifteen seconds;
that bound is stated in the panel, so a stale read on another replica right
after a save is not mistaken for a failed one. Reset deletes every rule
authored in the app and returns the install to exactly what the chart
declares. An install with no identity provider configured never sees the
panel — there is nothing for it to map. -
Personal API tokens: your scripts get a key of their own. Until now the
only credential the hub accepted was a browser session's cookie, so a script,
a CI job or a future client had nothing to authenticate with. Settings →
Access now mints named tokens with an optional expiry; sent as
Authorization: Bearer avurut_…, a token authenticates the request as its
owner. The secret is shown exactly once and only its SHA-256 is stored, so
no database read — and no admin — can recover it later; the list shows each
token's prefix and when it was last used, which is what "is this one still
needed?" actually takes to answer. A token deliberately carries no
permissions of its own: it resolves to its owner's live grants at request
time, so demoting a user demotes their tokens in the same moment and
disabling the account silences every token it holds. A bad, expired or
revoked token is a401, never a quiet fall-through to the anonymous role a
demo install grants cookieless visitors. Managing tokens requires only being
signed in — deliberately no role floor, so a user whose grants were all
revoked can still clean up the credentials they handed out, for the same
reason they can still log out — and a global admin can widen the list to
another user's tokens to audit them. Revoking a token you don't own answers
404, not 403, so the endpoint confirms nothing about other people's keys. -
The service map now says what is wrong, not just that something is.
A node used to turn red the moment any error appeared in the window — a
binary that never distinguished one failed health check from an outage. Its
ring is now the service's actual status (healthy, degraded, down, idle), read
from the same dependency-aware rollup the Service Health board uses. The map
deliberately does not re-derive those thresholds: they are configurable per
group and live in the hub, so a second copy in the browser would drift and the
two screens would quietly disagree. A service the rollup does not cover reads
as unknown — never as healthy.Edges carry real latency for the first time: p50 and p95 measured from the
caller's span, which is what that call path actually cost including
network and queueing. That is deliberately not the callee's own server-side
p95, which the node already shows, and the gap between the two is usually the
point — in the seeded demo a node readsp95 200mswhile the edge into it
readsp95 220ms, so 20ms is being paid somewhere the callee cannot see. It
costs one extra aggregate on a join the query already ran. Edges derived from
network flows have no span to measure, so they omit the field rather than
report a false0ms.Hovering a node fades everything outside its neighbourhood and labels its
edges with rpm, p95, error rate and TCP RTT where measured. Search, a
problems-only toggle and a group filter all live in the URL, so a narrowed map
is a link rather than a screen you describe over a call; zoom, fit and a
legend round it out. The status and group filters appear only when service
health is running, since both read its rollup — and on an install without it
the ring falls back to the previous error-presence signal rather than going
quietly blank.The carbon lens moved from the node border to a halo around it. The border was
the only one a node had, and the status ring now needs it; as a halo, a node
shows its health and its gCO2e at once instead of one overwriting the other.
The Dashboard's compact topology is the same component, so it gained all of
this without a second implementation to keep in step. -
One screen for how the estate is doing. Everything the product knew lived
behind a hypothesis you had to already have: traces if you knew the service,
nodes if you knew it was capacity, alerts if you knew something had fired.
Opening the app told you nothing until you had a guess. The Dashboard is now
the landing route and gives you one — service-group health, live topology
beside the firing alerts, and Kubernetes capacity, in three bands.It is fixed on purpose: no widget model, no layout editor, no persistence.
Every band reads an API that already existed, so the screen added no hub
surface at all, and each band follows its own module — bands whose module is
off simply do not mount, so the screen never shows a panel that would 404.
With service health off, the summary band falls back to the busiest services
and those fallback cards carry no status: thresholds and dependency
propagation belong to that module, and inventing a second set here would put
two answers to one question on the same screen.One honest gap: there is no CPU utilization percentage anywhere on it. Nothing
in the collection path reports allocatable CPU, so a percentage would need a
denominator the install does not have — capacity reports cores in use, and
only memory shows both halves of a real bar. -
Say which services matter, from the app. Service health groups — a name,
a criticality tier and the namespaces or services it covers — are now created,
edited and deleted in Settings → Groups, and apply to the next health read.
Until now the only way to define one wasserviceGroupsinvalues.yaml
followed by ahelm upgrade, which meant that in practice nobody did: the
Service Health board showed one auto-discovered group per namespace and the
tier lanes stayed empty. Auto-grouping still works exactly as before, so
nothing disappears while you organize, and the board now links straight to the
editor instead of naming a config key.Groups declared in the chart keep working and render read-only, because the
config wins a name collision — an install that manages its groups in Git must
not have them quietly overridden from a browser, so the conflict is refused at
write time rather than discovered at the next upgrade. Writes are admin-only
and go through the same validation the ConfigMap loader applies at boot, so
the API cannot store a group that would fail the next restart.The merge of the two sources happens in exactly one place, shared by the API
and the alerting evaluator. The evaluator does not go through the API, so
merging in a handler would have meant a group you created showing as critical
on the health board while alerting never paged on it — a divergence pinned by
a test that drives both paths and then fires a real rule
(design/2026-08-07-service-groups-crud.md). -
Two new Settings tabs: Storage, and Access.
Storage answers "where is my telemetry and how much of it is there".
The ClickHouse address, database and user, read-only — not as a missing
feature but because ClickHouse is the store, so it cannot hold its own
connection string; the card says so and gives the--setline instead of a
form that would be a lie. It is reported even while ClickHouse is
unreachable, which is when "what address did we fail to reach?" is the first
question. Then per-signal size, compression, row count, age and retention,
moved here from Status so each tab answers one question: Status is "is it
healthy right now", Storage is "what is in it".Retention now shows two numbers when they disagree. The days in your values
are what the install is configured to keep; the TTL on the tables is what
ClickHouse is enforcing, and changing a retention value does nothing to
tables that already exist until the migration re-applies it. Until then the
configured number is a wish, and the column says30d → 7drather than
repeating the wish. A freshly migrated database with retention not yet
applied reads→ none.Access shows which role may do what, per area of the product. Every cell
is derived by the hub from the authorization its routes registered with, not
written out a second time in the browser: routes register through an index
that records their guard, so adding an admin-only endpoint puts it in the
matrix and changing a guard changes the matrix with it. A table that can
disagree with the middleware is worse than no table, because it gets
believed. An install running without authentication says so at the top,
instead of presenting a model nothing is enforcing. -
Turn signals on and off from the UI, without a redeploy. Settings →
Collection becomes writable: an admin switches OBI traces, logs,
infra-metrics, profiling or energy collection on or off, and edits the
excluded-namespace list, and the sensor picks the change up in seconds. Until
now every one of those decisions meant editingvalues.yaml, runninghelm upgrade, and holding the cluster permissions to do it — so in practice
collection was whatever it was at install time. The screen also reports the
effective configuration (chart values with your overlay applied), so what
it shows is what the sensor is actually doing, and "reset to defaults" puts
the cluster back to exactly what the chart declares.Off by default (
collection.runtimeControl.enabled): opting in grants the
hub a deliberately narrow Role —get/update/patchon its own four named
sensor ConfigMaps andget/patchon the named sensor DaemonSet, in its own
namespace, and nothing else. The hub patches its own annotation to roll the
DaemonSet, leaving Helm's ownership untouched, so a laterhelm upgrade
behaves normally. With the flag off, nothing changes and no extra permissions
are granted.Proven end to end against a real cluster: the Helm smoke gate now writes an
overlay through the API, asserts it reaches the sensor ConfigMaps and rolls
the DaemonSet, then resets and asserts the cluster reconciles back. -
Find a pod on the Nodes screen. Both tables now sort by any column, and
both filter — nodes by name, pods by name, namespace or workload, with a
namespace picker that appears once there is more than one namespace to choose
between. On a real cluster the pods table is a hundred-plus unordered rows,
and until now the only way through it was the browser's find-in-page.
Filters live in the URL, so a narrowed view is a link you can send someone,
and they apply as you type — the rows are already in the browser, so nothing
waits on a query. When a filter is active the counts read "N of M", so a
narrowed table can't be misread as a shrinking cluster, and a filter that
matches nothing says so instead of showing the "install the sensor" empty
state, which would send you off to debug a perfectly healthy install.
Fixed
-
go test -race ./...ran out of time before it could finish the hub's API
suite. bcrypt cost 12 is a deliberate login-path choice, but the race
detector makes a hash-and-compare pair cost ~5.5s, and every handler test
that bootstraps an admin, logs in, or creates a user paid it.internal/api
spent 503s that way and crossedgo test's 10-minute per-package timeout on
CI — surfacing as a panic in whichever test happened to be running when the
clock ran out, which is why it read as a hang rather than as accumulated
cost. The cost now drops tobcrypt.MinCostinside ago testbinary and
nowhere else: the switch istesting.Testing(), which is false in every
production build, so no flag, environment variable or chart value can reach
the cheap cost. The dummy hash burned on the unknown-user path tracks the
same cost — bcrypt reads the cost from the hash, not from the caller, so
leaving it pinned at 12 would have kept that path slow and hidden half the
problem.internal/apinow runs in 4.7s andinternal/authin 3.3s; the
production cost of 12 is pinned by a test, as is the dummy's agreement with
it. -
The shared demo account was offered a password form it could never
submit. Settings → Account decided whether to render the change-password
form from the sign-in origin alone — and the demo viewer is a perfectly
ordinary local account, becauseEnsureDemoUsercreates it as one. So a
visitor to a demo install could open the tab, type a current and a new
password, submit, and only then be told the attempt was never possible. The
hub was right to refuse it (that row is re-created and re-keyed from the
install's configuration on every boot, so a "successful" change would
silently revert, and the credential is shared with every other visitor); the
UI simply wasn't told./api/v1/auth/menow carries apasswordChangefield
stating whether self-service rotation applies and, when it doesn't, why —
self,idp(the identity provider owns the credential) orshared(the
demo account). It reproduces the hub's own refusals in the same order, and
the Account tab renders the explanation instead of the form. A value this
build doesn't recognise renders the explanation too: offering a form the
server will reject is the failure being fixed, so the fallback is never the
form.
Security
- The green endpoints served any project's energy and carbon figures to an
unauthenticated caller.GET /api/v1/green/summary,/green/budgetsand
/green/reportwere registered with the bare handler wrapper instead of the
session middleware every other signal route uses. Nothing then put an identity
on the request — and the per-project scope check treats "no identity" as
"authentication is switched off", the branch that exists so an
auth.enabled=falseinstall keeps working. So on an install with
authentication on, those three routes answered 200 with no session at all,
for any tenant named in the request header: per-service energy in watt-hours,
carbon in gCO2e, monthly budget usage, and the CSRD-ready report export. They are
read-only, so nothing could be changed through them, but the data itself is a
fair map of what an estate runs and how hard. All three now require the viewer
role and honour project grants, like every other signal. A test enumerates the
project-data routes and asserts each answers 401 without a session — the gap
survived precisely because nothing asserted over the whole set.