v0.1.20 Group-scoped visibility end to end, scheduled analyses, cross-run findings search, API tokens bound to an owner and usable over REST, the Geo map page, mute from the alert row
📋 Full release notes: RELEASE_NOTES.md
Images (published by CI to GHCR):
ghcr.io/horryworks/yagra-core:v0.1.20
ghcr.io/horryworks/yagra-poller:v0.1.20
ghcr.io/horryworks/yagra-web:v0.1.20
Group scoping went from a type nothing consulted to a working control: the read surface filters by
it, an admin can hand one out, and both API tokens and /mcp honour it. Alongside that, the
Troubleshoot tools gained a cross-run findings search and scheduled runs, and the Geo map became a
real page.
Breaking changes
- An API token now acts as an account, and one whose owner cannot be resolved stops working.
Tokens used to be free-standing identities:api_tokenscarried its own role and had no link to
usersat all, so deleting, disabling or demoting the account that issued a token changed nothing
about the token. Migration0057binds each token to an owner, matching on thecreated_by
username. A token whose issuing account no longer exists cannot be matched, and no longer
authenticates — it is listed as "No owner" so it can be revoked deliberately. Re-issue any such
token, preferably owned by a service account (below). - The four Top-N endpoints now return an object, not a bare array.
GET /api/v1/metrics/top,/metrics/interface-top,/metrics/interface-deltaand
/alerts/top-nodesanswer{"entries": [...], "partial": false}; what used to be the whole body
is nowentries./metrics/interface-heatmapkeeps its shape and gains the samepartialfield.
partialistruewhen the ranking covers only the groups the calling account may see and rows
it is entitled to may be missing — a Top-N is ranked by the metric store, which knows nothing
about groups, so a scoped account's list is filtered afterwards and can come back short. It is
alwaysfalsefor an account with unrestricted visibility, which is every account today. The
WebUI is updated; an external client reading these endpoints must readentries. :latestnow means the latest stable release, not the development trunk. Until now every
push tomainpublishedghcr.io/horryworks/yagra-*:<sha>and moved:latest, so the default
indocker-compose.deploy.ymlanddocker-compose.poller.ymlhanded you a development build.
Development builds are no longer published at all — the registry holds releases and nothing else.
:latestmoves when a release is tagged without a-beta/-rcsuffix, and:<sha>exists only
for commits that were released. If you were followingmainthrough:latestyou are now
following releases; pin an explicit tag if you wanted something else. Note:latestfollows the
most recently pushed stable tag rather than the highest version, so a hotfix cut after a larger
release moves it backwards.docker-compose.deploy.ymltakes a newYAGRA_IMAGE_REPO, defaulting to
ghcr.io/horryworks. Leave it unset for published releases; it exists so a development machine
can point at a private registry holding unreleased builds.
New Features
- Group-scoped API tokens and
/mcpconnections work. The two refusals that stood in for
enforcement are lifted, and the promise made in v0.1.19 — "group scoping will be accepted again
when the read paths actually filter by it" — is kept.POST /api/v1/api-tokensaccepts
{"scope": {"Groups": [...]}}instead of answering400 unsupported_scope, and/mcpadmits a
group-scoped token instead of403-ing it. Every MCP tool now resolves the caller's scope and
applies the same rule its REST counterpart does: node lists and event searches filter in the
query, rankings and histories filter after, a tool naming an out-of-scope node answers exactly
what an unknown id answers, andrun_analysiscannot be launched over a scope the caller does not
hold. Settings ▸ API tokens gained a scope picker and a Can see column.
Two containment rules: a token can never exceed its owner, so a token owned by a group-scoped
account inherits that account's scope (narrowing the account narrows its tokens at once, with
nothing to re-issue) and giving such a token a different scope is refused with
400 owner_is_scoped; and a token scope must name groups that exist. To give a token a narrower
view than its owner, own it with a service account scoped to what the token should see. - Accounts can be limited to a set of node groups. Settings ▸ Users ▸ Change scope
(PUT /api/v1/users/{id}/scope) narrows what an account sees to the groups you pick and
everything beneath them;"All"restores the whole fleet. Enforcement across the read surface
shipped earlier in this release — this is the part that hands a scope out, so a scope is now
something an account can actually hold rather than a value nothing could be set to. A node in no
group stays visible only to unrestricted accounts, and a node outside the scope answers404(the
same answer an unknown id gets, so the scope cannot be used to probe for what exists). Saving a
scope signs the account out of its current sessions, the way a role change does — the scope is
captured in the session token, so a live one would keep the old, wider view.
Two rules worth knowing: an Admin cannot be scoped (409 admin_is_unscoped) because
administration is fleet-wide, and promoting an account to Admin clears whatever scope it held; and
a scope naming no groups is refused (400 empty_scope) rather than stored, since it would
otherwise be an account that signs in successfully to an empty inventory with nothing to explain
why.GET /api/v1/usersandGET /api/v1/auth/menow carryscope, and the account menu says so
out loud when the signed-in account is limited. SSO accounts are provisioned unrestricted and are
narrowed here; the stored assignment survives every subsequent login (mapping IdP groups to a
scope is a later increment). - Scheduled analyses. Troubleshoot ▸ Scheduled runs an analysis on a preset cadence — daily,
weekly or monthly at a time of day (UTC), over the whole fleet, a site or one node. Until now
every analysis had to be launched by hand, so a nightly anomaly sweep meant someone remembering.
GET/POST /api/v1/analysis/schedulesandPUT/DELETE /api/v1/analysis/schedules/{id}, all
Operator-and-up like launching a run.
Two behaviours worth knowing: a fire the runner's admission control refuses is deferred, not
skipped — the schedule stays due and the next minute's tick retries, rather than losing a whole
period to a busy moment — and the traffic-flow analyses cannot be scheduled on a deployment
with no flow store, because each fire would write an empty run forever. A schedule defaults to
not notifying, unlike a run you launch and wait for. - Saved findings — search what the analyses found, across every run. Troubleshoot ▸ Saved
findings (GET /api/v1/analysis/findings) lists findings from every analysis, newest first,
filterable by node or site, by analysis, by severity and by time window. Until now a finding was
only reachable through the run that produced it, so "has anything been found about this switch
lately" meant opening runs one at a time. Rows link back to the run's report. The endpoint is
keyset-paged: pass the last row'satandidback asbeforeandbefore_id. - The REST API accepts an API token. Until now a
yat_…token authenticated/mcpalone and
the REST API answered401, so unattended automation had to store a password and log in on every
run. A token now names the surfaces it may be presented at, and one that includesrestworks
on/api/v1exactly like a session token:Authorization: Bearer yat_…. Existing tokens carry
mcpalone, so upgrading cannot turn a credential minted for an AI client into one that can
reconfigure monitoring — reaching REST is an explicit choice made when the token is issued.
Two limits apply to a token wherever it is used: it cannot administer users (a credential that
could mint its own successor would outlive every revocation of the original), and endpoints that
identify the signed-in account —GET /api/v1/auth/me, the personal dashboard — answer403. - API tokens have an owner and an optional expiry. A token's effective role is
min(token role, owner's current role), so demoting an account narrows its tokens at once, and
disabling or deleting an account revokes them. Expiry is optional:POST /api/v1/api-tokenstakes
expires_at, and omitting it still means no expiry — appropriate for a service account driving an
integration. Settings ▸ API tokens shows the owner, the surfaces, the expiry and why a token is
refused when it is. - Service accounts.
POST /api/v1/userstakeskind: "service"— a machine account with no
password that cannot sign in through either the local form or SSO. It exists to own API tokens, so
an integration keeps working when the person who set it up changes teams, and so that disabling it
stops every credential it owns at once.passwordis now optional in that request body and is
refused for a service account rather than ignored. The lock-out guard that protects the last
admin now counts only accounts a human can sign in with, so a service account cannot become the
only administrator. YAGRA_PAT_OIDC_IDLE_DAYS(default30) bounds how long an API token owned by an
SSO-provisioned account survives its owner's silence. Yagra is never told when an identity
provider disables an account — the accounts table is only refreshed by a successful SSO login —
so an absent owner is the only signal available. Local and service accounts are unaffected.- URL monitors can present credentials. A new
http_authcredential kind covers Basic, Bearer
and a custom header; bind one to a URL monitor and the poller presents it. The credential is
envelope-encrypted at rest and inlined into the poll job at dispatch time, the same path SNMP
credentials already take — the poller never reads a credential store. The existingapi_token
kind, which until now was creatable and consumed by nothing, is accepted as a bearer token.
A monitor that presents credentials must verify TLS (400 credential_needs_tlsotherwise). - Pollers hand their nodes over when they shut down. A poller now sends a final heartbeat
markedleavingon SIGTERM, so core drops it from its pool's hash ring immediately and
reassigns its nodes. Previously a shutdown was indistinguishable from a network partition, so
core waited out three missed beats (30s) — and if the restart finished inside that window the
ring never changed at all and those nodes went unpolled for the whole restart. Rolling upgrades
now hand over in seconds without the operator doing anything. - Monitoring gaps say what passive data was lost. A gap row now records which passive
listeners the poller had bound (syslog:514,trap:162, …). Polled metrics are backfilled from
the poller's buffer on reconnect; syslog, traps and flow exports are not, so this is the
difference between an unexplained silence in the event log and a known loss. (SNMP informs are
the exception — the sender retries until acknowledged.) - The global search box in the top bar works. It has been present but permanently disabled
since the shell was built, with a code comment saying no search endpoint existed —
GET /api/v1/nodes/searchhas existed since the node picker was added. It searches nodes,
debounced, with arrow-key navigation,Ctrl/Cmd+K and/to focus, and Enter to open the
node. The popover states that only nodes are searched: alerts, events and groups have no
server-side search, and a nodes-only result set that looks fleet-wide is worse than none.
Mobile gets it as a tap-to-open row under the top bar. - Topology ▸ Geo map is a real page. It was a "Coming soon" placeholder; it now draws a pin per
node group that has coordinates, on a world outline, coloured by that group's worst member state,
with wheel/drag/pinch pan-zoom and click-through to the group's nodes. It reads the same
per-group health rollup the dashboard's Geo map widget does, so the two cannot disagree about a
site. The outline is bundled in the app — no tile server, no external request, no new dependency
— because a monitoring console is what you open when the network is broken, and a map that needs
the internet is blank exactly when it matters. - Group coordinates can be set again.
PUT /api/v1/node-groups/{id}/geowas removed in
v0.1.19 as an uncalled endpoint, but the dashboard's Geo map widget still read those
coordinates — leaving a widget with no way to be populated. The endpoint is restored and the
group dialog now has latitude/longitude fields. - "Notify me" on a Troubleshoot run now notifies. The choice was offered and stored, and
consumed by nothing. A completion notice now appears from any page, with a link to the report.
It fires only while Yagra is open in this browser, which the control now says. - Active alerts can be muted from the row. Alerts ▸ Active gains a working Mute action that
opens the mute dialog with the node fixed and the metric that fired pre-filled, so the mute
covers exactly the check being triaged rather than the whole node. It appears for operators and
admins — the rolesPOST /api/v1/mutesalready accepts — and is absent for viewers instead of
offered and rejected. Muting suppresses notification only: the alert stays in the list and in
the history, unchanged. - The permanently disabled "Open external" action is gone. It had been shipped disabled since
it was written, promising a deep link into PagerDuty/JSM that no configuration could ever supply
— Yagra stores those integrations as outbound endpoints, not per-incident URLs. Theackedpill
already names the tool and the person who acknowledged, which is the honest version of the same
information. No API changed.
Bug Fixes
- An API token with the
restsurface was refused by most read endpoints. Anything that filters
by group scope — the node lists, the fleet summary, alerts, events, metric rankings, topology —
resolved the caller through the session store alone, so a validyat_…token answered
401 unauthorizedeven though the same token passed the permission guard on the same request.
Introduced with group-scope enforcement earlier in this release, so no tagged version shipped it.
Both guards now read the one credential each request resolves, and a test pins that a token
reaches a scoped read. - Editing a URL monitor cleared its credential binding. The form's own comment said every
field is sent explicitly because the request is a replace, and then omitted the credential.
This was invisible while nothing consumed the binding; with the feature above it would have
logged a monitor out on any unrelated edit. - Filtering the node inventory returned at most 100 matches, silently. The API clamped the
limit to 500 and documented that as the maximum, while the query re-clamped to 100 — so a filter
matching thousands of nodes showed 100, with nothing indicating the list had been cut. The cap is
now a single constant (500) used by both the edge and the query, and the tree shows a notice when
a filter fills the page. - The WebUI could keep serving a pre-upgrade page after an upgrade. nginx sent no
Cache-Controlfor the SPA at all, so browsers fell back to heuristic freshness (RFC 9111
§4.2.2) and could reuse a cachedindex.htmlfor hours. Because each image replaces the whole
document root, that stale page names hashed assets the new image no longer contains — and the
SPA fallback answered those requests withindex.html, so the browser rejected a script served
astext/htmland rendered nothing, with no indication why.index.htmlis nowno-cache
(revalidated on every load, still a 304 when unchanged), hashed assets under/assets/are
immutablefor a year, and a missing asset returns 404 instead of HTML.