docs/plans/2_19/runner-version-platform-uptime.md
Make the runners page actually useful for diagnosing a fleet. Today the
runners table tells the operator that a runner is "alive" (via the touched
timestamp) and not much else. When something goes wrong — a task hangs on a
specific runner, a runner stops picking up jobs after a server upgrade, a
runner silently runs an outdated binary — there is no way to triage from the
UI. This plan adds three concrete pieces of information that answer the most
frequent questions an operator has:
In scope:
Out of scope:
GET /api/runner on every poll
(handled by RunnerController.GetRunner in api/runners/runners.go).
That call is the natural place to attach the metadata. The runner sends
version, os, arch and started_at as request headers (or, if a
request body is preferred, in a small JSON payload on the existing
UpdateRunner progress call). Headers are preferred because they require
no change to the encryption path used for the GetRunner response.runner table: version, os,
arch, started_at. All nullable — older runners simply don't report
them. The server overwrites these fields on every successful poll, next to
the existing touched update.Runner JSON shape returned by /api/runners and
/api/project/:id/runners with the new fields. They are omitted when
null so an older runner appears as "unknown" in the UI rather than as a
zero value.web/src/views/Runners.vue:
systemInfo.version).os/arch (e.g. linux/amd64, darwin/arm64).started_at to now. Tooltip shows
the absolute timestamp.runtime.GOOS, runtime.GOARCH, the embedded build
version, and time.Now() as the runner's started_at.X-Runner-VersionX-Runner-OSX-Runner-ArchX-Runner-Started-At (RFC3339)RunnerController.GetRunner, read the four headers. Treat missing
headers as "unknown" (do not error — older runners must keep working).[a-z0-9_]+ regex, started_at parses as RFC3339 and is not in the
future.db.RunnerManager.TouchRunner (or add a sibling
TouchRunnerWithInfo) to write the new columns in the same UPDATE that
bumps touched. One write, no extra DB load.version, os, arch, started_at columns to
the runner table for all three SQL dialects (MySQL, Postgres, SQLite).
Add equivalent fields to the Bolt model.db.Runner struct with the four new fields (JSON-tagged, all
omitempty / pointer-typed where appropriate).api-docs.yml reflects the additions.web/src/views/Runners.vue getHeaders(), add version, platform,
uptime columns. Slot templates:
item.version: text + warning chip when item.version !== version
(the existing version computed property already strips the build
suffix from the server version).item.platform: ${item.os}/${item.arch} or — when missing.item.uptime: relative duration (dayjs(item.started_at).fromNow(true)
or equivalent helper already in the codebase). Tooltip with the absolute
timestamp.— when the field is
missing so mixed fleets (old + new runners) look clean.—.—.—.| Risk | Mitigation |
|---|---|
| Runner forging headers to misrepresent version/platform | The runner is already authenticated via its token; this is informational, not a security boundary. Same trust level as touched. |
started_at clock skew between runner and server | Stored as the value reported by the runner; rendered relatively. Acceptable: a few seconds of skew is invisible at "uptime" granularity. |
| Schema churn when we later add more fields (hostname, etc.) | New columns are independent and additive; each future field follows the same pattern without disturbing this one. |
| UI clutter on narrow screens | Existing table already has a horizontal scroll affordance; the three new columns are short. If needed, hide Platform behind a tooltip on Version. |
| Header size on hosts behind aggressive proxies | All four headers together are well under 256 bytes — no realistic risk. |