Skip to content

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

Choose a tag to compare

@horryworks horryworks released this 02 Aug 18:21
· 78 commits to main since this release

📋 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_tokens carried its own role and had no link to
    users at all, so deleting, disabling or demoting the account that issued a token changed nothing
    about the token. Migration 0057 binds each token to an owner, matching on the created_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-delta and
    /alerts/top-nodes answer {"entries": [...], "partial": false}; what used to be the whole body
    is now entries. /metrics/interface-heatmap keeps its shape and gains the same partial field.
    partial is true when 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
    always false for an account with unrestricted visibility, which is every account today. The
    WebUI is updated; an external client reading these endpoints must read entries.
  • :latest now means the latest stable release, not the development trunk. Until now every
    push to main published ghcr.io/horryworks/yagra-*:<sha> and moved :latest, so the default
    in docker-compose.deploy.yml and docker-compose.poller.yml handed you a development build.
    Development builds are no longer published at all — the registry holds releases and nothing else.
    :latest moves when a release is tagged without a -beta/-rc suffix, and :<sha> exists only
    for commits that were released. If you were following main through :latest you are now
    following releases; pin an explicit tag if you wanted something else. Note :latest follows 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.yml takes a new YAGRA_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 /mcp connections 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-tokens accepts
    {"scope": {"Groups": [...]}} instead of answering 400 unsupported_scope, and /mcp admits a
    group-scoped token instead of 403-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, and run_analysis cannot 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 answers 404 (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/users and GET /api/v1/auth/me now carry scope, 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/schedules and PUT/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's at and id back as before and before_id.
  • The REST API accepts an API token. Until now a yat_… token authenticated /mcp alone and
    the REST API answered 401, 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 includes rest works
    on /api/v1 exactly like a session token: Authorization: Bearer yat_…. Existing tokens carry
    mcp alone, 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 — answer 403.
  • 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-tokens takes
    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/users takes kind: "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. password is 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 (default 30) 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_auth credential 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 existing api_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_tls otherwise).
  • Pollers hand their nodes over when they shut down. A poller now sends a final heartbeat
    marked leaving on 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/search has 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}/geo was 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 roles POST /api/v1/mutes already 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. The acked pill
    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 rest surface 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 valid yat_… token answered
    401 unauthorized even 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-Control for the SPA at all, so browsers fell back to heuristic freshness (RFC 9111
    §4.2.2) and could reuse a cached index.html for 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 with index.html, so the browser rejected a script served
    as text/html and rendered nothing, with no indication why. index.html is now no-cache
    (revalidated on every load, still a 304 when unchanged), hashed assets under /assets/ are
    immutable for a year, and a missing asset returns 404 instead of HTML.