Repository navigation
Tags: zechtz/vertex
Tags
v2.4.2 — A dashboard that admits when it is stale The service grid is fed entirely by the /ws socket, and nothing ever reopened it. A sleep, a server restart or a brief network drop ended the feed for the life of the tab, and the dashboard carried on rendering the last states it heard. A stopped service still showed green, indistinguishable from a running one, and the only way to find out was to reload — the same reload the session work in v2.4.0 existed to remove. The realtime feed The socket now reconnects itself, backing off from a second to a cap of thirty so a server that is down is not hammered while a machine waking from sleep is back almost at once. Reconnecting is only half of it. The server sends no snapshot on connect — websocketHandler adds the client to its broadcast list and nothing more — so everything that happened while the socket was down was missed and is never resent. Services are refetched on any reopen after the first, which is what actually makes the view current again. While the feed is down the toolbar says so. A permanent "connected" light would be noise; silence while disconnected is misinformation. Three more faults sat in the same effect. The socket was rebuilt every time a service was selected, because selectedService was read inside onmessage and so had to be a dependency; those reads are now functional state updates and the socket mounts once. Log entries accumulated without limit for as long as the tab stayed open, and are capped at a thousand per service. JSON.parse ran unguarded on every frame, so one malformed message threw inside the handler. Dialogs Two things were already solved in components/ui and not used. The Modal primitive handled Escape, backdrop clicks and scroll lock, but was invisible to assistive technology and leaked focus: the page behind a modal was inert to the eye and not to the keyboard, so tabbing walked out of the dialog into controls nobody could see. It now identifies itself as a dialog, is labelled by its own title, keeps Tab inside itself, and hands focus back to whatever opened it. Six call sites still used window.alert and window.confirm while the app had toast and confirm-dialog for exactly this. Native dialogs block the page, ignore dark mode and look nothing like the rest of the app. Failures are now error toasts, the two destructive actions go through the styled confirm, and the two "coming soon" notices are info toasts. Six dialogs moved off their own overlays onto the primitive: system metrics, service actions, topology, uptime detail, user profile and wrapper management. The primitive grew 3xl through 7xl widths and a node title first, because the dialogs that must follow need both. Still outstanding Eighteen dialogs continue to build their own overlay, and so still miss Escape, the scroll lock, the dialog role and the focus trap. The six that moved are verified by typecheck and build, not by eye.
v2.4.1 — Services you can reach offline Turning the internet off left the client unable to reach any service. The per-service Eureka overrides that made offline work possible were still read at startup, but nothing could set them. Eureka registration overrides The feature had landed in two halves. 52e117c added it whole on feat/local-dev — the database columns, the load and save paths, the API field, and the panel in the service config modal — but that branch was never merged. What reached main was 811d12c, a cherry-pick of an unrelated Maven wrapper fix that happened to carry the environment variable injection out of operations.go and nothing else. main gained the reader without the writer. The gap surfaced only as a build failure. 81764bd added the two models.Service fields so the package would compile again, and noted at the time that the feature was inert. It stayed inert: EurekaPreferIPAddress was always nil and EurekaHostname always "", so both injection branches were unreachable and the config server's prefer-ip-address won unopposed. Offline, that resolves to a hostname nothing can reach. The missing half is now in place — the columns and their migration, the scans and writes in every load and save path, the field on ServiceConfigRequest and its mapping in UpdateService, and the override panel in the modal. Set prefer-ip-address on a service and it registers by IP, which survives losing the network. Session renewal across a sleep The renewal prompt could appear for a token that had already expired, and Continue then failed with no way forward but a reload — the behaviour the feature exists to remove. The warning had been scheduled with a single setTimeout up to a day ahead, which does not survive the machine sleeping. The phase is now polled, so a sleep is noticed on wake, and a token past its expiry hands over to the sign-in prompt rather than offering a renewal that cannot succeed. Upgrading Both Eureka fields are unset by default, so no service changes behaviour until prefer-ip-address is set on it. Existing rows migrate to NULL, which reads back as no override, and the migration re-runs as a no-op.
v2.4.0 — Sessions that do not interrupt you An expired session used to make actions quietly stop working. Nothing said why, and the redirect to the login page only happened on the next full page load, so the way to discover it was to reload — which is not something a user should have to think to do. Two layers now cover it. Before expiry The token states its own expiry, so the app schedules a warning locally rather than discovering the problem by having a request fail. Five minutes out, a prompt offers to continue; one click renews the session in the background, with no password and no interruption. POST /api/auth/refresh issues the new token, and requires a still-valid one — an expired session goes back through login, so this extends an active session rather than reviving a dead one. After expiry If nobody was watching the tab, the session still ends. A 401 no longer fails the request: it is parked, the app is told once however many requests failed together, and each is replayed once the user signs back in — so the restart they clicked still happens. The prompt appears over the page rather than redirecting, because a redirect discards scroll position, open dialogs, filters and half-typed input, and leaves the user to remember what they had been doing. Underneath, every authenticated request now goes through one place. Six modules previously read the token from localStorage and called fetch themselves, and almost none recognised a 401 — one had the handling written with the logout call commented out. Upgrading No action required. Existing tokens keep working and pick up the warning at their next expiry.
fix(ui): show the version actually running in the sidebar The sidebar read "v2.0.0", hardcoded. The binary reports its version from a git tag linked in at build time, so the two were never going to agree - the running build is v2.2.7-1-gcc75c75. Serve the linked-in build information from GET /api/version and read it in the sidebar, so there is one source of truth and nothing to remember to update at release time. Build info is passed to NewHandler rather than set afterwards: there is a single call site, and a constructor argument cannot be silently forgotten the way a setter can. The version string needs normalising for display. The Makefile strips the "v" prefix when building an exact tag but git describe keeps it otherwise, so a tagged build yields "2.2.7" and an untagged one "v2.2.7-1-gcc75c75"; rendering either directly gives "2.2.7" or "vv2.2.7". formatVersion strips any prefix and adds exactly one back. If the request fails the line is omitted rather than falling back to a constant, since showing a wrong version is what this replaces. Commit and build date are available as a tooltip. (cherry picked from commit e74c4c8fe5c2067335fdd5834627ec5111b6195f)
v2.2.7 — Configuration access no longer freezes
Fixes the intermittent freeze where Vertex stayed reachable — login kept
working — but configuration and service views hung for minutes, sometimes
until a restart. Login survived because authentication talks straight to
the database; everything routed through the service manager did not.
Four independent causes, each serialising unrelated work behind a single
blocking operation.
Database
SQLite ran in rollback-journal mode, where every write takes an
EXCLUSIVE lock on the entire file and blocks every reader. It now opens
in WAL with an explicit busy timeout and a bounded connection pool.
Log lines are persisted in batches instead of one INSERT per line, and
the daily cleanup runs as small batched transactions rather than one
long transaction that held the write lock from start to finish — and no
longer takes application locks while that transaction is open.
Manager locking
GetServices held the manager lock while waiting on individual service
locks, and service locks were held across HTTP health checks. Because
sync.RWMutex is writer-preferring, one slow service plus one queued
writer parked every subsequent reader — including configuration reads,
which touch no database at all. A service lock is now never taken while
the manager lock is held, and no blocking I/O happens under one.
Websockets
Broadcasts wrote to clients while holding the global client lock with no
write deadline, so a browser tab that stopped draining its socket could
pin that lock — and the service lock above it — until restart. Writes
now happen outside that lock, bounded by a deadline, behind a
per-connection write lock.
Also fixes a data race where the log broadcaster read the service map
under the wrong mutex.
Build
The release image moved off golang:1.23-bullseye, whose security
Release file expired when Debian 11 left LTS on 2026-08-31 and broke
every build with apt exit code 100. It now builds on
golang:1.23.10-bookworm, and the docker job gained the QEMU setup its
linux/arm64 leg requires.
Upgrading
Existing databases migrate to WAL automatically on first start.
Log churn leaves behind free pages that SQLite never returns to the
filesystem. If your database has grown, reclaim it while Vertex is
stopped:
sqlite3 vertex.db "VACUUM INTO 'vertex.new.db'" && mv vertex.new.db vertex.db
A 3.6 GB database compacted to 73 MB in testing — 98% of it was
freelist left over from log deletion.
PreviousNext