Releases: cropalato/promview
Release list
v0.1.0-beta.3
A small release for the desktop client: links in an alert's details open in
the browser, and the tray says which version is running and whether a newer
one is out. The server and the console are unchanged from beta.2.
Added
- desktop: the tray menu's top line shows the running version,
Promview 0.1.0-beta.3, and opens the release list. When a newer release is out it becomesUpdate available: … (running …)…and opens that release. The client checks GitHub's releases at startup and every twelve hours; a client on a final release is only offered final releases. It only tells you — nothing is installed and nothing pops up, because Tauri's updater cannot replace a deb, rpm or pacman install.update_check = falsein the config file turns it off.
Fixed
- desktop: links that ask for a new window, such as an alert's Generator URL and Alertmanager URL, open in the system browser. A webview has nowhere to put a
target="_blank"link, so clicking them did nothing. Onlyhttpandhttpsaddresses are handed to the browser; anything else is refused. - desktop: the page the browser lands on after signing in tries to close its own tab. Browsers usually refuse, because the tab was not opened by a script and the identity provider has added history entries, so the "you can close this tab" message stays as the fallback.
Server image: ghcr.io/cropalato/promview:v0.1.0-beta.3
Helm chart: oci://ghcr.io/cropalato/charts/promview
The desktop installers below are unsigned. Windows SmartScreen
warns on first run, and macOS is not built at all: signing needs
certificates this project does not have yet.
v0.1.0-beta.2
The second beta adds two ways to sign in without an identity provider — local
accounts and an LDAP directory — lets open mode grant more than viewer for a
lab where everyone is trusted, and gives administrators a console view for
role bindings, closing one of the three gaps recorded for beta.1.
That stretches what beta claimed: the feature set was called settled, and this
release adds two authentication paths to it. They are the newest code here and
the least exercised by anyone but the author, which is worth weighing before
pointing one at a production directory. The two remaining known limits are in
the README. The server is now built with Go 1.27 on Alpine 3.24; building from
source needs Go 1.26.
Added
-
auth: open mode can be told to grant more than viewer.
PROMVIEW_OPEN_MODE_ROLEacceptsviewer(the default),operatororadministrator, andPROMVIEW_OPEN_MODE_AUTHORnames what actions are recorded under. It is for a lab or a test instance where everyone who can reach the port is already trusted and a sign-in is friction with nothing behind it — the case where open mode was previously most crippled, because acknowledging a test alert required an identity the deployment had deliberately chosen not to have.What it costs is attribution. Acknowledgements, assignments, notes and Alertmanager's
createdByall record the author string rather than a person, so nothing done in an elevated deployment can be traced to anybody;administratoradditionally lets any reader rewrite who has access, and the bindings they write outlive the lab. Promview says so in a startup warning, a second and distinct one foradministrator, a non-dismissible console banner, and thepromview_open_mode_elevatedgauge — which is the one that matters, because a warning printed once at startup is not something anybody watches, and a lab setting that rode into production is precisely what nobody is looking for.Making this work meant the capability checks stopped short-circuiting on whether anybody signed in, and are decided by the grants alone. That cannot widen what a signed-in principal can do:
resolvePrincipalnever marks one anonymous, so no OIDC, LDAP or local principal could ever have reached those branches. The default is unchanged in every respect — a plain open-mode deployment still reads and does nothing else, and there are tests holding exactly that, since it is what an over-broad change would break. -
auth: an
ldapauthentication mode, using search-then-bind: a service account searches for the user, then the connection binds again as that user with the password they typed. The directory performs the password check, so Promview never sees or stores a password hash. An empty password is refused before anything is dialled, because an LDAP simple bind with an empty password succeeds as an anonymous bind on most servers — which turns "leave the password blank" into a valid sign-in as anybody, and is the most common way an LDAP integration is wrong. The username is RFC 4515 escaped before it reaches the search filter; unescaped, a*matches every user and)(uid=*rewrites the filter into one of the caller's choosing, either of which turns a login form into a directory dump.ldaps://is required unless StartTLS is enabled or the host is loopback, and StartTLS never falls back to cleartext.A directory that cannot answer is reported as unavailable rather than as a wrong password — a failed service bind, a failed search, or a filter matching two people would otherwise send an operator hunting for a typo in somebody's credentials during an outage. A search matching two entries is refused rather than resolved, since picking one signs somebody in as whichever entry sorted first. The directory's own error text never reaches the caller, because the filter it quotes contains the username.
Role bindings gain an
ldap_groupsubject kind, using the samesubjectIssuerandsubjectGroupfields OIDC groups already use. The issuer's URL scheme decides which kind a binding is, so there is no separate flag to forget, and a binding whose scheme contradicts its kind is refused rather than stored: one that can never match looks exactly like access somebody has. Group names are stored lowercased, as common names by default or as full distinguished names for directories whose CNs collide across OUs;promview access inspectshows what the directory actually reported, which is how an operator learns what to type. The identity's subject isentryUUIDorobjectGUIDwhere the directory has one, because a DN changes when somebody is moved between OUs and a DN-keyed identity would silently become a second user with none of the first one's bindings.Groups can be read from an attribute on the user entry or by searching the directory for groups that name the user as a member. The second is not a fallback for exotic directories:
memberOfis an overlay plenty of OpenLDAP and FreeIPA installations do not enable, and without it a sign-in there succeeds with no groups at all — which resolves to no roles and answers "your account has no access", sending the operator to the wrong person to fix it. That search runs while still bound as the service account, because running it as the just-signed-in user uses whatever rights that user happens to have, which in a default OpenLDAP is not enough to see the groups organisational unit at all.Sign-in attempts are throttled per username before the bind is forwarded. Without that, this login form would be a way to drive every account in the directory into its own lockout policy.
github.com/go-ldap/ldap/v3is the one new runtime dependency; hand-rolling BER/ASN.1 parsing of network input would be considerably worse. -
auth: a
localauthentication mode, for a deployment with no identity provider to point at. Accounts are created withpromview user createand live in Promview's own database; roles are granted by user ID rather than by group, because a user ID is assigned when the account is created and a values file cannot know one ahead of time. Passwords are hashed with PBKDF2-HMAC-SHA-256 at 600,000 iterations in a self-describing encoding that carries its own cost, so raising it later upgrades each password on its owner's next sign-in rather than requiring a flag day nobody can perform. Argon2id would be the better algorithm and is deliberately not used: the standard library has no memory-hard KDF, so it would mean takinggolang.org/x/cryptoas a direct dependency and compiling it in. At this scale that trade is not obviously worth making — the attacker holding the credentials table also holds every Alertmanager token and live session hash in the same database, so cracking a password buys access they already have. The$scheme$prefix in the stored format means adding$argon2id$later is an addition rather than a migration.Every sign-in failure answers identically — unknown username, wrong password, disabled account, locked account — and every one costs a full password derivation, including the unknown-username case, which derives against a decoy generated from random bytes at startup. A missing account that answered faster than a wrong password is a free list of which usernames are real. An account that exists and has no read role is the one deliberate exception: it answers 403 so the console can say so, which only tells an attacker something they have already proved they know.
Attempts are throttled per username and per address, before the password is hashed rather than after — a limiter that runs afterwards has already paid for the attack it is refusing — and a semaphore bounds concurrent derivations, because a 600,000-iteration KDF makes an unthrottled login endpoint the cheapest way to saturate the server somebody is watching an incident on. Repeated failures also back off in the database, doubling from the fifth attempt and capping at fifteen minutes. That lock always expires on its own: one that had to be cleared by hand would be a denial of service against a named on-call operator, triggerable by anyone who knows their username, at the moment they need to acknowledge a page.
promview user unlockends one early.promview_login_attempts_totalis labelled by mode and outcome, with a duration histogram beside it. Brute force against an endpoint nobody counts is invisible: the server stays healthy, the logs stay quiet, and the only trace is a spike in a series that does not exist. -
web: role bindings can name an LDAP group. The subject-kind control offers it alongside OIDC groups and users, and the issuer field is checked against the kind before the request is sent — an
ldap_groupbinding needs anldap://orldaps://issuer and anoidc_groupbinding anhttp://orhttps://one. The server refuses the other pairing anyway, and is right to: a binding whose scheme contradicts its kind can never match anybody, yet sits in the list looking exactly like access somebody has, which is worse than an issuer that is obviously wrong because nobody goes looking. The console also stopped folding every unfamiliar subject kind intooidc_group, which would have labelled an LDAP binding as an OIDC one — and that label is what an administrator reads to decide whether a binding grants anything. -
web: the console signs in with a username and password where the deployment keeps its own accounts. The form clears the password from component state the moment the request settles, success or failure, because a password left in state is a password in a heap snapshot and in the React devtools tree for as long as the tab is open. A refusal is shown exactly as coarsely as the server meant it — one message covers a wrong password, an unknown username, a disabled account and a locked one — since narrowing it in the browser would rebuild the oracle the server refuses to be. A throttled attempt is the one case with something useful to...
v0.1.0-beta.1
The first beta. Every item on the project's own first-release list is
implemented — ingestion, lifecycle, filtering and grouping, resumable SSE,
acknowledge, assign, close, notes, silences, bulk actions, OIDC with
label-scoped roles enforced in SQL, and now authorization administration over
the API — and the scale the plan commits to is measured rather than assumed.
Beta means the feature set is settled and the known gaps are written down, not
that this has been proven in production. Three are recorded in the README:
concurrent load is unmeasured, binding administration has no console UI, and
cmd/promview is the thinnest-tested package. The published alpha image tag
freezes at 0.1.0-alpha.40; beta is the moving pointer from here.
Added
-
testing:
make load-testmeasures the scale the project plan commits to — up to 50,000 active alerts and 100 received per second — which nothing had ever checked. It seeds the full set in the batches a real Alertmanager delivery arrives as, then times the reads an operator waits on. Against PostgreSQL 18.6 on a developer machine it ingested 50,000 alerts at 1,236 per second, twelve times the committed rate, and answered the first page with its severity counts in 100ms, a label-matched page in 47ms and a grouped page in 355ms. It is gated onPROMVIEW_LOAD_TESTand out ofmake verify: it writes tens of thousands of rows and takes minutes. The assertions are deliberately generous — a load test that fails on a slow laptop teaches people to ignore it — so they catch an order-of-magnitude regression and nothing finer, and the printed numbers are the real output. What it does not measure is recorded alongside what it does: it drives the store directly, with one client and no concurrent readers, so the HTTP path, the SSE fanout and contention between ingestion and reads are all excluded. -
httpapi: role bindings can be administered over the API, which until now meant a shell command on the server.
GET /api/v1/access/bindingslists them with their matchers,PUT /api/v1/access/bindings/{name}creates or replaces one, andDELETEremoves it. Administrator only — an operator is refused too, because changing who can do what is not an operator action, and open mode's anonymous reader has no name to attribute a policy change to. The listing includes matchers where the existing diagnostics view omits them: a scope is half of what a binding means, and showing the role without it would describe a different binding than the one in force. The path names the binding, and a body naming a different one is refused rather than resolved, so a PUT to one name can never rewrite another. This was the last item on the project's own first-release list. -
auth: a change that would leave a deployment with no administrator binding is refused, whether by deleting the last one or demoting it. The check runs inside the same transaction as the write, so two administrators removing each other at once cannot both succeed, and it lives in the store rather than a handler because a check made outside the transaction is a race for the ability to administer the deployment at all. Creating the first administrator where there is none is always allowed, which is how an open-mode deployment adopts OIDC. The CLI writes through the same methods, so the shell is not a way around the rule.
-
web: the console can reach closed alerts. Closing removed an alert from the list with no way back, which from an operator's seat is indistinguishable from losing it; an Open/Closed control beside the silence filter is the route back. It is a pair rather than a trio because the server answers with open alerts or with closed ones and cannot return both, and an "All" that quietly showed half of what it promised would be worse than not offering one.
-
web: the alert drawer can assign, close and note. Three actions had shipped in the API with no way to use them: an assignee field (free text, because an alert is routinely handed to somebody who has never signed in here), a close/reopen button worded as filing rather than resolving since Alertmanager is never told, and a notes panel that reads oldest first — the order a handover is read in. Notes offer no edit or delete because the API offers neither, deliberately, and a control that cannot work is worse than its absence. A note written during an earlier occurrence is marked as such, so it does not read as describing the current incident.
-
web: alerts can be selected and acted on together. A checkbox column and a bulk bar above the list carry acknowledge, close, assign and note over the selection, which is what the bulk endpoints existed for: the alternative is an operator clicking through forty rows after one incident. The selection survives the stream-driven refreshes that arrive constantly on a busy console — ticking forty rows while alerts keep landing must not have the work emptied underneath — and ids that leave the page are dropped, because acting on an alert the operator can no longer see is not what they selected. The bar reports all three outcomes separately:
unchangedis not folded into success, andnotFoundis worded as not visible to you rather than refused, because the server deliberately does not distinguish an alert outside a scope from one that does not exist and saying "forbidden" would leak what that hides. Assign and note ask for their value inline in the bar rather than through a browser prompt: a note is written in sentences and needs the same textarea the single-alert composer has, and a prompt returns before there is anywhere to put a refusal or a validation failure. In the grouped view the members of an expanded group carry the same checkbox; a collapsed group does not, because its members are not loaded and the bulk API takes explicit ids rather than a filter, so a group checkbox could only ever act on a subset it had not shown. The rule is that you can select what you can see. The selection is pruned against what is actually on screen in each view, so a selection made inside a group whose members sit past the flat page is not silently dropped. -
web: a closed alert says so. The list and the drawer both carry a
closedchip beside the state rather than instead of it, because closing is Promview-local and the source may still be reporting the alert as firing; the drawer also says who filed it and when, the way it already does for an acknowledgement. Without this an alert reached by deep link, or found through the Closed filter, read as an ordinary firing one — and theclosedfield the client had been parsing since the filter landed went unused. -
desktop: two tests assert that an event the shell has never heard of still reaches the page, at both layers that could drop it: the SSE parser and the message the webview is handed.
stream.gapwas added to the server after this shell shipped, and a desktop that filtered on event names would have left its console resuming from a cursor the server had already deleted, silently. The behaviour was already correct; nothing proved it. -
httpapi: the per-alert actions envelope carries
canNote. It resolves to the same operator check as the others today, but a console that inferred the right to write a note from the right to acknowledge would silently follow that flag the day one of them stops meaning the same thing.
Fixed
- web: a console resuming from a cursor that stream retention has deleted now takes a fresh snapshot instead of carrying on. It did not subscribe to
stream.gap, so a console left disconnected past the retention window reconnected, reported no error, and showed state that was quietly wrong about what was firing. It retries rather than scheduling a live refresh: a live refresh merges into what is already held, and what is already held is exactly what is no longer trustworthy. The client advances its own cursor past the deleted range before announcing it, so a reconnect in that window does not resume from the same dead position and get told about the same gap again.
Server image: ghcr.io/cropalato/promview:v0.1.0-beta.1
Helm chart: oci://ghcr.io/cropalato/charts/promview
The desktop installers below are unsigned. Windows SmartScreen
warns on first run, and macOS is not built at all: signing needs
certificates this project does not have yet.
v0.1.0-alpha.40
Added
- alerts: every operator action now has a bulk form —
POST /api/v1/alerts/bulk/acknowledge,PUT /api/v1/alerts/bulk/assignee,POST /api/v1/alerts/bulk/closeandPOST /api/v1/alerts/bulk/notes— each taking the same body as its single-alert counterpart plus the ids to apply it to. The alternative is an operator clicking through forty rows after one incident, which is how alerts stop being acknowledged at all. Selection is by explicit id and never by filter: "close everything matching this query" reads the same whether it matches four alerts or four thousand, and the operator cannot see which until it has happened. At most 500 ids, the same ceiling as a page, because a selection is made from one. Every alert is judged on its own and reported on its own: one outside the operator's scope does not fail the rest, and comes back asnotFoundrather than forbidden — the same answer the single-alert endpoint gives, so a bulk reply cannot be read to discover what exists outside a scope.unchangedis reported apart fromappliedso an operator can tell "I changed forty" from "I changed two and the rest were already done", and the status is 207 rather than 200 when anything came backnotFound, matching how a partly applied group silence answers. The request is one transaction, since a bulk action is one decision and half of it surviving a failure is a state nobody asked for. Validation is shared with the single-alert paths rather than reimplemented, so a note written to forty alerts is held to exactly the rules one written to a single alert is.
Documentation
- Documentation that had fallen behind the code is brought up to date: the metrics reference gained
promview_stream_gaps_totalandpromview_stream_events_pruned_total, the authorization guide stopped describing the operator role as acknowledge-only when it now covers assign, close, note and silence, the Helm chart gained astream.retentionvalue so the retention window is configurable on Kubernetes at all, and the Docker Hub listing's environment table gainedPROMVIEW_STREAM_RETENTION. The project plan no longer lists assignment, close and notes as planned work, and says plainly what is left: the console can display an assignee and a note count but has no controls to use any of the three, and does not act onstream.gap.
Build System
make docs-checkfails when an installation example names a release older than the current one, and runs in CI. This had drifted four separate times — the Docker Hub listing shipped a version 33 releases old, the README's Helm example six, the Kubernetes guide nine — and each was a reader following an instruction that installed something other than what the page described. Image tags in examples now follow the movingalphapointer, which cannot go stale; chart versions have to be exact, so those are what the check watches. Prose naming a version historically is deliberately not checked: "carried in values since 0.1.0-alpha.35" is a fact about the past and rewriting it would be wrong.
Server image: ghcr.io/cropalato/promview:v0.1.0-alpha.40
Helm chart: oci://ghcr.io/cropalato/charts/promview
The desktop installers below are unsigned. Windows SmartScreen
warns on first run, and macOS is not built at all: signing needs
certificates this project does not have yet.
v0.1.0-alpha.39
Added
- alerts: an operator can file an alert as handled.
POST /api/v1/alerts/{id}/closewith{"closed":true}closes it andfalsereopens it. Closing is Promview-local and never reaches Alertmanager — it is not silencing, and the source keeps reporting the alert exactly as before. It is a flag rather than a fourthstatusfor that reason:firing,resolvedandexpiredare claims about what the source reports,closedis a claim about what somebody decided, and an alert can honestly be both still firing and already dealt with. Closed alerts leave the default list, since closing an alert that stayed in it would be an action with no visible effect;?closed=truefinds them again. Besides an operator reopening it by hand, a delivery that materially changes the alert reopens it — new labels, a new annotation, a status transition — because that is not the alert that was closed. An identical repeat deliberately does not: those arrive everyrepeat_intervaland carry no new information, so reopening on one would mean a close never outlived the next notification. - alerts: an operator can leave a note on an alert — what was checked, what was ruled out, who was called.
POST /api/v1/alerts/{id}/notesappends one; notes appear on the alert detail oldest first, which is the order a handover is read in. They are deliberately append-only, with no endpoint that edits or deletes one: a note is what somebody relied on at the time, and a handover that can be quietly rewritten afterwards is worth less than none. Each records the occurrence it was written against, so a note about a previous incident stays attributable instead of reading as though it describes the current one — and unlike the assignment and the acknowledgement, notes survive an alert resolving and firing again. They live in their own table rather than in alert history, because every history entry is written by promview about an event while a note is written by a person about a judgement, and flattening the two would bury the sentence somebody typed among a hundred generated ones. The list payload carries anotescount rather than the notes themselves, filling the console column that has been waiting for it; the list's job is to show there is something to read, and opening the alert is what reads it. - alerts: an operator can record who owns an alert.
PUT /api/v1/alerts/{id}/assigneesets it and an empty assignee clears it, which is why it is a PUT rather than two verbs: the request states the assignment in full, and sending it twice leaves the same owner. The assignee is free text rather than a reference to a Promview user, because an alert is routinely handed to somebody who has never signed in — a vendor, a team rota address, the name in a runbook — and a foreign key would turn every one of those into an error instead of an assignment. The operator who decided is recorded separately asassignedBy, since "who owns this" and "who said so" are different questions and the second is the accountability trail.assigneeis on the list payload as well as the detail, because "what is on my plate" is asked of the list; the console already had a column waiting for it and now fills it. Assigning to the existing owner writes nothing and wakes no console. Assignment is cleared when a resolved alert fires again, alongside the acknowledgement: that is a new occurrence, and the previous owner never agreed to own it.
Server image: ghcr.io/cropalato/promview:v0.1.0-alpha.39
Helm chart: oci://ghcr.io/cropalato/charts/promview
The desktop installers below are unsigned. Windows SmartScreen
warns on first run, and macOS is not built at all: signing needs
certificates this project does not have yet.
v0.1.0-alpha.38
Added
- stream: stream events are now deleted once they pass
PROMVIEW_STREAM_RETENTION, default 24h,0to keep everything. They exist only so a client that lost its connection can resume, which makes almost all of them dead weight within minutes, and nothing had ever removed one. A day covers the disconnections a resume is actually for — a closed laptop, a rolling deploy, a proxy that dropped every connection at once — and past that a fresh snapshot is cheaper than replaying history. The sweep shares the expiry sweep's ticker rather than adding a knob: a retention window is measured in hours and the interval enforcing it in minutes, so the exact interval never mattered.promview_stream_events_pruned_totalcounts what it removes. - stream: a client resuming from a cursor that retention has deleted is now told so, with a
stream.gapevent carrying its own cursor and the oldest point the stream can serve. This is the half that made deletion safe to ship at all: the events between are gone, and handing back only the survivors would have left a console reconnected, reporting no error, and quietly wrong about which alerts are firing — worse than the unbounded growth being fixed. The correct response is a fresh snapshot, resumed from itsstreamCursor. A client at or past the watermark has missed nothing and is never interrupted, so a caught-up console is not asked to discard a view that is still correct. The watermark is a stored column rather thanmin(id), because the table being emptied completely is exactly when the question matters and exactly whenmin(id)has no answer.promview_stream_gaps_totalcounts them, and a rising count means the window is shorter than the disconnections a deployment actually sees.
Server image: ghcr.io/cropalato/promview:v0.1.0-alpha.38
Helm chart: oci://ghcr.io/cropalato/charts/promview
The desktop installers below are unsigned. Windows SmartScreen
warns on first run, and macOS is not built at all: signing needs
certificates this project does not have yet.
v0.1.0-alpha.37
Added
- packaging: the published image now carries a moving
alphatag pointing at the newest pre-release, so trying Promview no longer starts with finding out which alpha is current. It is applied only to tags carrying a hyphen, which is exactly the set the metadata action's ownlatest=autodeclines to movelatestfor: the two pointers can never name the same image, and neither claims to be something it is not.lateststill appears by itself at the first release without a pre-release suffix, and there has not been one. - packaging: the Docker Hub listing is now generated from
docs/dockerhub.mdand pushed by its own workflow whenever that file lands onmain. The listing was the one published artifact nothing in this repository owned, and it had drifted accordingly: it told readers todocker pull YOUR_DOCKERHUB_USERNAME/promview:0.1.0-alpha.3, pinned a Helm chart 33 releases old, and promisedlinux/arm64images that have not been built since alpha.12 — so anyone on Apple Silicon or arm64 nodes read a published promise and got an exec format error. Sourcing it from the tree makes a wrong claim there an ordinary diff. It is deliberately not part of the release workflow: a documentation fix should not wait for the next tag, and failing to update prose should not paint a release red. It authenticates with the sameDOCKERHUB_TOKENthe release job pushes images with, and skips when that secret is unset rather than failing a fork that has no credential. - packaging: the desktop client can be published to the AUR as
promview-desktop-bin, so Arch users can install and update it with a helper instead of downloading an installer from each release. Publishing is held behind theAUR_PUBLISHrepository variable and is off by default: it needs a maintainer account, and the AUR closed new registrations while this was written. Until that variable is set the job is skipped, so a release neither publishes nor fails on it. The AUR distributes recipes rather than packages, so this is a second PKGBUILD rather than a reuse of the onedesktop-archalready builds: that one repackages a deb sitting beside it in a CI workspace, which the AUR rejects twice over — a source entry without a URL has to be committed to the package repository, and the repository refuses any blob past a quarter of a megabyte. The published recipe names the release asset by URL and pins its digest, which is safe because uploaded assets are byte-stable, unlike the source tarballs GitHub generates. It also installs the license, which the existing package never did: itsinstallline was guarded with|| trueand pointed at a file that was never among its sources, so it silently shipped nothing. Publishing runs after the release job rather than beside it, since the recipe downloads an asset that job uploads.
Fixed
- alerts: reconciliation now revives an alert expiry retired while its Alertmanager was still holding it. Expiry infers an ending from a source going quiet, and a silenced alert is quiet by design — Alertmanager sends no notifications for one — so a maintenance window would retire alerts that were demonstrably still firing, and nothing brought them back. Measured against production on 2026-08-19, that was 30 alerts hidden from the console while live and suppressed on the source. Reconciliation now reads a source's
expiredalerts alongside its firing ones and returns tofiringany the live view still carries, clearing theends_atexpiry invented. The alert keeps its occurrence and its acknowledgement: a new occurrence is what follows aresolvedalert, where the source itself said it ended, and nothing ended here — ingestion has always treated a webhook for an expired alert as an ordinary update, and reconciliation now agrees with it.resolvedalerts are not examined, and an Alertmanager reporting nothing revives nothing. The revival streamsalert.updatedrather thanalert.created, because the console raises a desktop notification for a newly created critical and a deployment correcting thirty wrong expiries should not announce thirty new alerts. - alerts: an alert its Alertmanager still holds is no longer expired in the first place. Reviving alone would have cycled — the silence that caused the wrong expiry is usually still in force, so the alert would be retired again one window later and revived again indefinitely — so reconciliation records
reconciled_aton every alert it confirms live, and expiry now measures staleness from the later of that andlast_seen. This is a new column rather than a wider reading oflast_seen, which means one specific thing, when a webhook last delivered the alert, and is also the console's default sort key and its pagination cursor: restamping it every pass would reshuffle the table under a reader and carry rows across cursor boundaries. Expiry is unchanged where it is still the only signal, since a source with no Alertmanager URL or one that cannot be reached has noreconciled_atmoving. The expired half of the reconciliation query is restricted to fingerprints the live reading actually carries: expired alerts accumulate, nothing retires them, and selecting all of them would make every pass scan a set that only ever grows in order to decide almost every time that there is nothing to do. - security: three dependency advisories, all reachable from code this project runs.
github.com/jackc/pgx5.7.6 carried a SQL injection through placeholder confusion with dollar-quoted string literals (GO-2026-5004), reachable from the migration runner;github.com/go-jose/go-jose4.0.5 panics on certain JWE decryption (GO-2026-4945), reachable from OIDC ID token verification, where a panic in the authentication path is a way to take the server down without credentials; andrustls0.23.43 in the desktop client accepted TLS 1.3 handshake messages across encryption level boundaries (RUSTSEC-2026-0285), published two days before it was found here. Upgraded to 5.9.2, 4.1.4 and 0.23.45. Nothing in the repository was scanning for any of this, which is the more useful half of the finding; see thevulnjob below.
Build System
make changelog-checkverifies that every released version in the changelog still extracts as release notes, and runs as its own CI job. The extraction script otherwise runs once per release and nowhere else, which is the trap the release workflow's comments already describe: the first time a change to it is exercised would be the release that depends on it. It checks the changelog end too — a heading that drifts from the## [version] - dateshape, or a version whose section is empty, is a release that would publish notes saying nothing. All 34 released versions pass.- Release notes are now the changelog entry for the version being tagged, followed by the image and chart references and the unsigned-installer warning that were previously the whole of them. Every release said the same three sentences about packaging and nothing about what had changed, while the changelog said it all in the one place a reader of a release never looks.
scripts/changelog-section.shextracts one version's section, and the release job checks out the tree it never used to need. A tag whose version has no changelog entry is annotated as a warning rather than failing the job: thin notes are not a reason to withhold the binaries. - Advisory scanning now runs on every change as the
vulnjob, covering all four toolchains:govulncheckfor Go,npm audit --omit=devfor both npm projects, andcargo auditfor the Tauri crate.--omit=devbecause the question is what ships — a vitest advisory is worth knowing and is not a vulnerability in the console anyone runs. Scanning ismake vulnlocally and is deliberately not part ofmake verify: it reads a database over the network, and an advisory published this morning should not fail a local run of work that has nothing to do with it. In CI, going red is exactly the point. - Go tests now also run under the race detector, as
make test-raceand its own CI job. The server fans one ingestion out to every open console over SSE and runs the expiry and reconcile loops beside it, so its concurrency is not incidental and was never being checked. It is a separate job so a data race reports as a data race rather than as "the Go job failed", and out ofmake verifybecause the race build is several times slower. All 237 tests pass under it. - CodeQL analyses Go and TypeScript on every change and again weekly, with the
security-and-qualityqueries. The weekly run matters as much as the per-change one: queries improve after code is written, so the same commit is worth re-scanning later. Rust is left to clippy andcargo audit. - CI ran every job twice for any branch with a pull request open on it, because
pushcarried no branch filter alongsidepull_request. Pushes are now watched onmainonly, where commits land in this project, and everything else arrives as a pull request. A concurrency group cancels superseded runs on every ref exceptmain, where each commit is a state someone may later bisect to and deserves its own answer. - Dependabot now watches all five dependency manifests, none of which had anything watching them: the Go module, both npm projects, the Tauri crate, the Dockerfile's base images, and the workflows' own action versions. Minor and patch updates are grouped into one pull request per ecosystem, because a project this size cannot absorb one pull request per dependency and a queue nobody reads updates nothing. Major versions stay ungrouped, since those are the ones worth reading. GitHub's vulnerability alerts, Dependabot security updates, and private vulnerability reporting are enabled alongside it — the last is what
SECURITY.mdsends reporters to.
Documentation
- The project has community health files for the first time:
CONTRIBUTING.md,SECURITY.md,CODE_OF_CONDUCT.md, i...
v0.1.0-alpha.36
Server image: ghcr.io/cropalato/promview:v0.1.0-alpha.36
Helm chart: oci://ghcr.io/cropalato/charts/promview
The desktop installers below are unsigned. Windows SmartScreen
warns on first run, and macOS is not built at all: signing needs
certificates this project does not have yet.
v0.1.0-alpha.35
Server image: ghcr.io/cropalato/promview:v0.1.0-alpha.35
Helm chart: oci://ghcr.io/cropalato/charts/promview
The desktop installers below are unsigned. Windows SmartScreen
warns on first run, and macOS is not built at all: signing needs
certificates this project does not have yet.
v0.1.0-alpha.34
Server image: ghcr.io/cropalato/promview:v0.1.0-alpha.34
Helm chart: oci://ghcr.io/cropalato/charts/promview
The desktop installers below are unsigned. Windows SmartScreen
warns on first run, and macOS is not built at all: signing needs
certificates this project does not have yet.