Skip to content

Wigo 1.0.0 - #39

Merged
carsso merged 63 commits into
masterfrom
v1
Aug 29, 2026
Merged

Wigo 1.0.0#39
carsso merged 63 commits into
masterfrom
v1

Conversation

@carsso

@carsso carsso commented Aug 29, 2026

Copy link
Copy Markdown
Member

The whole roadmap, and the version that says so.

Merging this publishes the release. VERSION is 1.0.0 and no v1.0.0
tag exists, so once the CI is green on master the release workflow creates the
tag, the GitHub release with the three .deb, and pushes the multi-arch image
to ghcr.io.

What is in it

Alerting. Acknowledgements and silences, flap detection, re-notification
and escalation, quiet hours, and host dependencies — a router going down is one
message rather than forty. Apprise targets route by group, host, or label.

History. Metrics and status changes are kept locally in the SQLite that was
already there, each bounded by its own retention. That is a decision, not a
fallback: wigo answers about its own past without a second system being up,
which is exactly what a second system is least likely to be at the moment
somebody is trying to find out what broke. The Prometheus exporter is a way
out, not the way in.

Labels. [Labels] on a host, filterable through
/api/hosts?labels=env=prod,role=db, listed with counts by /api/labels, and
shown on the host page as links to the fleet filtered on them. Additive rather
than a replacement: Group is untouched and travels as the label group, so
filtering by label reaches hosts that have never heard of labels.

Interface. SSE with polling kept as the net, level/text/label filters, dark
mode, graphs, a status timeline, a wall for a screen on a wall, and a drawer on
phones. Contrast fixed — the WARNING badge was white on yellow at 1.63:1.

Probes. check_ssl_cert, check_systemd, check_dns, check_docker
(podman too, by config) and check_backup_age, each verified against real
failures rather than only the happy path. Per-probe timeouts, since
interval - 1 is a ceiling and not a wait.

Foundations. Martini dropped for the standard library router, real
authentication with roles and tokens, an OpenAPI document checked against the
code by a test, an OCI image, and a Go test suite.

Compatibility

An older wigo on either side keeps working, and that is tested in both
directions: a host encoded here decodes on a master that only knows
Name/Group/Status, and a host arriving without labels gets none invented while
staying filterable by its group.

One regression this branch introduced and fixed before landing: the standard
library router answered 404 to /api/hosts/, which Martini had always
accepted. Every script written against the trailing form would have broken
silently — and a 404 from a monitoring API reads as that host is gone.

carsso and others added 30 commits August 23, 2026 12:59
Counter toggles, search and theme, with the filter state in the URL so a
view is shareable. Refresh no longer stacks requests when a load outlasts
the interval.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Groundwork for driving probes from the UI: the postinst re-seeded symlinks
on every upgrade, undoing admin choices, and the directory bookkeeping
raced. Also fixes a list.Remove loop that cleaned one probe per cycle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zero is not a level of the scale, so a master that only aggregates remotes
rendered as ERROR. Fall back to 100 when there is no probe to look at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed gates

No endpoint calls these yet. A probe name becomes a path, so it is matched
against an allow list rather than sanitised. Both gates default to closed,
so an upgrade exposes nothing new.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Disabled probes have no result to attach anything to, so GET /api/probes
reads the directory on demand. Fixes a latent bug where repitching a probe
deleted the result the new directory had just produced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A probe silently not running is what an operator needs to notice, so the
disabled ones get their own cards and a banner counting them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Forwarded to the remote's own API, so both ends have to agree: a master with
write actions off refuses to act as a jump host onto the whole fleet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A NAT'd client polls for orders on the connection it already holds open. A
new RPC method rather than a changed one, which gob tolerates both ways.
Silence is a refusal, so upgrading a server opens not one client.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On thirty machines the one turned off eight months ago was invisible. Hosts
that could not be read are named: quietly skipping one would be worse than
no list at all on a page whose whole job is to be exhaustive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One style cannot serve both a solid card header and a plain table row, so
the caller says which surface it is on rather than the component guessing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Also turns write actions on in the development configuration, since the
scheduling controls are hidden without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The write path for those hosts was built, the read path never was. A client
now reports its whole schedule : a disabled probe produces no result, and
results were all it used to send.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Asking for an interval means asking the probe to run once. Moving one copy
and leaving the others would keep it running from them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
probes/disabled/ made "disabled" a place rather than a state, which left the
fifteen probes wigo ships without enabling outside the model. Disabling puts
a probe back in examples/ rather than deleting it, so nothing is lost.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It left the disabled list without arriving in the running one, since a probe
that has not run yet has no result. Choosing an hourly interval meant an
hour of nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exit code 13 discards the result and stops the schedule, which looks exactly
like a probe enabled a moment ago. Wigo already tracked these, nothing ever
exposed them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Metadata only : the probes directory still decides what runs, and the expiry
loop can only ever start a probe. Without a deadline an afternoon of
maintenance quietly becomes eight months.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An hourly probe that just went critical is an hour of not knowing whether
the repair worked. Also the way back for a probe that exited 13, which
until now really did need a restart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A known outage kept spamming Apprise for hours, and the only way out was
disabling the probe, which stops the monitoring too.

An ack records the status it was taken at: anything worse still gets
through, and recovering clears it. A silence must have an end date.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Something oscillating OK to CRITICAL every minute sent two notifications a
minute, burying the real incident. It is now called out once and left
alone until it settles.

Settling takes dropping to half the threshold, so a probe sitting on the
boundary does not flap in and out of the flapping state itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A probe that broke at 3am and is still broken at 9am produced exactly one
message, and six hours of silence look like six hours of everything being
fine.

Quiet hours delay rather than drop: a held notification is not recorded as
sent, so the repeat loop says it once the window closes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scraping a master gets the fleet, so there is no exporter to deploy
alongside. Probe metrics were lost unless OpenTSDB was configured.

Format written by hand: the client library would pull in a dependency tree
larger than this file for a name, some labels and a number.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only amd64 and armhf were produced, which leaves out every 64 bit arm
board and server. Also drops the probes/disabled directory the package
created, which no longer means anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Debian slim rather than scratch: the probes are Perl scripts, so a
distroless image would start, answer, and monitor nothing.

The probes directory is a volume seeded on first start only, or a restart
would undo every probe an operator enabled or disabled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
net/http has matched method and path patterns since Go 1.22, which is all
martini was providing here. Three unmaintained dependencies removed, none
added.

Adds the server timeouts martini never set, and real security headers: the
middleware it replaces was called with no options, so every branch of it
was off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One shared credential was tolerable while everything was read only. It
stopped being once the API could disable a probe or silence a host:
anyone handed the dashboard to look at a graph could switch the
monitoring off.

The secret is readable once and stored hashed. The shared credential
stays an operator, so an upgrade locks nobody out of their own install.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A probe going critical waited up to a minute to appear. The event says
what changed, not what it changed to, so the browser refetches and one
serialisation of the state stays enough.

The periodic refresh stays: a stream can die quietly, and a page that
looks up to date while having stopped refreshing is worse than no stream.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A load average was thrown away the moment the next run replaced it unless
an OpenTSDB was configured, so "was it already climbing an hour ago" had
no answer.

Each wigo keeps its own history and a master reads a remote's through its
api: storing the fleet twice is what pushes people to a separate stack.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Plain SVG rather than a charting library, which would be the largest
dependency in the tree for a line and two axes.

Colour follows the series and not its rank, so hiding one from the legend
never repaints the others; the min-max band keeps the spike a bucket
averaged away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
carsso and others added 28 commits August 29, 2026 13:20
Linked in two, it was executed twice at two rates, each overwriting the other's
result and corrupting the deltas the stateful probes keep in /tmp. Smallest
interval wins; the extra links are reported, not deleted.
A wigo that serves anonymous readers never sends a 401, so the browser is
never challenged and the credential is unreachable. /api/login provokes it.
These two routes had no check at all, which stopped being harmless once
anonymous callers could read. The role only, not AllowWriteActions: that flag
is about this host's own probes, and every push master has it off.
Nobody guesses that a ban sign leads to disabled probes. Naming them costs a
click and saves hovering each one to find out which is which.
The whole mix was two strings, both the sidebar toggle. An i18n layer for two
strings would have been the larger mistake.
FileServer sent no Cache-Control, so the browser invented a lifetime for
index.html and then asked for a bundle the next build had deleted.
Direct parents only: reachability is transitive on its own, so a chain needs no
walking and cycles cannot happen. The recovery is held too, or forty hosts come
back announcing problems nobody was told about.
Exit codes a scheduler understands, --json, --group, --status, --watch.
Filtering prunes the tree before rendering, so the summary, the json and the
exit code agree by construction.
Every other way goes through the api, which needs a credential, which is what
a first token is for. Opens the database and nothing else.

The Makefile named main.go, so a second file in the same package was silently
left out of the build.
…e up on

Writing them turned up worse: a conf.d file replaced the probe's defaults
instead of merging over them, so naming one key dropped all the others.
A frozen page and a healthy fleet look the same, so the age of the last
refresh is in the header and turns amber. A failed request keeps the previous
tiles: an empty grid says there is nothing, which is not the same.
Compared to the registered routes in both directions on every build, plus a
reference check that caught six pointers to a schema nobody wrote.
The WARNING badge was white on yellow at 1.63:1. Ten links had no href, so
nothing reached them by keyboard: the ones that navigate are router-links now.
The collapsed rail was 56px of identical icons. Closing on choice compares
matchMedia to the CSS breakpoint : a sticky table header inflates innerWidth
to 881 on a 390px screen, so the old >= 768 check never fired.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
interval-1 is a ceiling, not a wait : a wedged probe held its slot for five
minutes while the dashboard showed the result from before the outage. Longer
than the interval is refused, since the next run fires regardless.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scroll was replayed at the end of every load, and a load happens each
minute and on every stream event. It now aims only on arrival, settling while
the charts still grow, and yields the moment the reader scrolls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The vendored Net::NTP was prepended to @inc, so it shadowed the very package
the deb depends on, three versions behind. The deb tree is now wiped before
each build, or the removed file would ship forever.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No check_podman : podman answers the same ps --format, so it is a config value
rather than a second script nobody would run. Both verified against real
failures -- crash loop, failing health check, a backup that wrote zero bytes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A Group has to be chosen -- par1, or prod, or db -- and the other questions
stop being answerable. Group is untouched and travels as the label "group",
so filtering by label reaches hosts that have never heard of labels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A host has one group, so a target watching every prod database had to name
them one by one. Labels are looked up when the notification is sent, not
carried on it, or a recovery would route by what was true an hour ago.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each label on a host page links to the fleet filtered on it. Which hosts match
is asked of the server, since group summaries carry no labels and one
definition serves the filter and the notification routing both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The deb dependency was added without the Dockerfile's own list, so check_dns
would have shipped in the image without the module it needs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Martini treated /api/hosts and /api/hosts/ alike ; the standard library router
that replaced it answers 404 to the second, so every script written against
that spelling broke silently on upgrade. Trimmed, not redirected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The old ones were hosted on user-attachments and predated most of the ui.
These are versioned next to the code, and every hostname, group and
measurement in them is invented -- nothing real is shown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The loop named stretch, buster and bullseye alongside them ; the repository
serves bookworm and trixie. The readme only mentioned bookworm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It only fired on master, on pull requests and by hand, so a branch was known
green after it landed rather than while it was being worked on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sharing a concurrency group built the head once instead of twice, but the
cancelled run kept the name of the required check and landed on the commit
being merged. Branch protection then refused the merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@carsso
carsso merged commit 04c289f into master Aug 29, 2026
2 checks passed
@carsso
carsso deleted the v1 branch August 29, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant