Documentation
¶
Overview ¶
templ: version: v0.3.1020
The health dashboard mounts github.com/larsartmann/go-health-dashboard under `tq serve` (2026-09-16 adoption): a live severity-grouped view of derived queue health at /health, plus the conventional JSON probes (/healthz, /readyz, /startupz). The checks are the store-backed subset of `tq doctor` — database, worker heartbeats, expired leases, DLQ depth — computed from the same queue.Store projection the dashboard already reads, never from process state.
templ: version: v0.3.1020
Package webui serves a read-only, live-updating web dashboard over the task queue: a pure projection consumer of the store's facts and task views (ADR-0003). One journal tailer fans out coalesced change notifications; each connected browser receives server-rendered HTML fragments over SSE and swaps them into the page by container id.
The package cannot mutate the queue: the worst failure is a stale dashboard, never journal corruption.
Index ¶
- Constants
- Variables
- func Board(data DashboardData) templ.Component
- func DeadLetterTable(data DashboardData) templ.Component
- func FactFeed(data DashboardData) templ.Component
- func FilterBar(data DashboardData) templ.Component
- func MetricsRow(data DashboardData) templ.Component
- func Page(data DashboardData) templ.Component
- func StatusCards(data DashboardData) templ.Component
- func TaskDetail(data DashboardData, t task.Task, facts []journalFactView, ...) templ.Component
- func TaskDetailPage(data DashboardData, detail taskDetailData) templ.Component
- func TaskNotFoundPage(id string) templ.Component
- func TaskTable(data DashboardData) templ.Component
- type BoardBandGroup
- type BoardColumn
- type BudgetView
- type Config
- type DashboardData
- type FilterState
- type Hub
- type ProjectSummary
- type ProjectionPump
- type Server
Constants ¶
const ( HealthDashboardPath = "/health" HealthSSEPath = "/health/sse" HealthSDKPath = "/health/datastar.js" HealthFaviconPath = "/health/favicon.svg" HealthLivenessPath = "/healthz" HealthReadinessPath = "/readyz" HealthStartupPath = "/startupz" )
Health route paths, mounted by Handler and asserted by the tests.
const ( DefaultAddr = "127.0.0.1:8090" DefaultPoll = 500 * time.Millisecond DefaultHeartbeat = 15 * time.Second // BannerPrefix and BannerReadOnlySuffix define the serve startup // banner's exact shape — shared by the CLI print and the e2e/smoke // parsers, which nearly broke twice over string drift (00:21 e3). BannerPrefix = "tq: dashboard on http://" BannerReadOnlySuffix = " (read-only)" BannerWritesSuffix = " (writes ENABLED: cancel/rescue from the UI)" )
Default configuration values.
Variables ¶
var ErrTokenRequiredOnLAN = errors.New(
"webui: refusing to serve on non-loopback addr without a token " +
"(the dashboard exposes task payloads and error tails; " +
"bind 127.0.0.1 or pass --auth-token / $TQ_SERVE_TOKEN)",
)
ErrTokenRequiredOnLAN is returned by Config.Validate and Server.Run when the address binds beyond the loopback interface without an AuthToken. The dashboard is read-only by construction (ADR-0003) but renders every task payload and error tail, so non-loopback binds are default-deny.
Functions ¶
func Board ¶
func Board(data DashboardData) templ.Component
Board renders the kanban projection (inner content of #frag-table when the filter selects the board): one column per lifecycle status — the queue's state machine made visible, cards newest first. Columns are read-only projections, not drag-and-drop lanes: moving a card is queue mutation and waits for the --allow-writes gate (ADR-0003 Phase D).
func DeadLetterTable ¶
func DeadLetterTable(data DashboardData) templ.Component
DeadLetterTable renders the dead-letter queue (inner content of #frag-dlq).
func FactFeed ¶
func FactFeed(data DashboardData) templ.Component
FactFeed renders the live fact tail (inner content of #frag-feed): the journal itself, terminal-style. Newest facts land at the bottom. Each line's task id links to the detail page — a thin custom variant of the library Scrollback (which renders plain text lines only); the classes mirror its markup so the visual language stays one.
func FilterBar ¶
func FilterBar(data DashboardData) templ.Component
FilterBar renders the search/filter form (inner content of #frag-filters).
func MetricsRow ¶
func MetricsRow(data DashboardData) templ.Component
MetricsRow renders the overview's two pure-SVG charts (M19): the fact rate sparkline (facts per 5-minute slice of the last hour) and the completion histogram (time-to-complete: queue wait + run, honestly labeled). Both are server-rendered SVG — no JavaScript, CSP-safe.
func Page ¶
func Page(data DashboardData) templ.Component
Page is the full dashboard page (ADR-0003): a live read-only projection of the journal. Fragments are server-rendered into the #frag-* containers so the page works without JS; SSE then replaces innerHTML.
func StatusCards ¶
func StatusCards(data DashboardData) templ.Component
StatusCards renders the overview stat band (inner content of #frag-stats). The four active states carry tone icons and link to their filtered view; cancelled and total stay quiet (no icon). Marker classes (card-pending, …) are kept as stable hooks for tests and CSS.
func TaskDetail ¶
func TaskDetail(data DashboardData, t task.Task, facts []journalFactView, prov priorityProvenanceView) templ.Component
TaskDetail renders the per-task detail page body. The two live regions sit in #frag-detail / #frag-timeline containers so the /task/{id}/events stream can swap them over SSE exactly like the dashboard fragments. The payload section sits BETWEEN them, deliberately static: a payload is immutable after enqueue, so an open prompt or raw pane survives every live swap, and the SSE stream only re-renders what can actually change.
func TaskDetailPage ¶
func TaskDetailPage(data DashboardData, detail taskDetailData) templ.Component
TaskDetailPage is the per-task detail page.
func TaskNotFoundPage ¶
TaskNotFoundPage is the styled 404 for /task/{id}: an unknown id keeps the dashboard chrome and a way back instead of a bare error page.
func TaskTable ¶
func TaskTable(data DashboardData) templ.Component
TaskTable renders the live task table (inner content of #frag-table).
Types ¶
type BoardBandGroup ¶ added in v0.3.1
BoardBandGroup is one priority band's slice of a board column's cards (ADR-0015): the band as the board's secondary grouping under the status lane, newest-first within the band.
type BoardColumn ¶
BoardColumn is one lifecycle column of the board view: the true count of tasks in this status (under the filter's project/query scope) plus the newest boardColumnLimit cards. Truncated is how many older tasks the column omits (the "+N older" link opens them in the table view).
type BudgetView ¶
BudgetView is the daily agent-spend projection: enqueued today vs the operator-set cap. Nil in the snapshot when no cap is configured.
func (BudgetView) Tone ¶
func (b BudgetView) Tone() display.StatTone
Tone picks the card's semantic color: green under 75%, amber under the cap, red at/over it.
type Config ¶
type Config struct {
// Addr is the TCP address to bind. Default 127.0.0.1:8090.
Addr string
// Poll is the journal tail interval. Default 500ms.
Poll time.Duration
// Heartbeat is the SSE keepalive interval. Default 15s.
Heartbeat time.Duration
// Version is the application version stamped into every health
// response and the dashboard's Version stat card. Empty leaves the
// card on the dashboard's "unknown" fallback — cmd/tq always passes
// the ldflags-injected main.version, so only in-package test
// constructors see the fallback.
Version string
// RequestLog enables per-request access logging (method, path, status,
// duration) via slog at Info level. Off by default.
RequestLog bool
// DailyBudget mirrors the agent pool's --daily-budget so the dashboard
// can show the spend card (tasks enqueued today vs cap). 0 hides the
// card — the cap is an operator decision, not queue state.
DailyBudget int
// AuthToken, when set, requires every request (pages, API, SSE, static)
// to present the token via an Authorization: Bearer header or a `token`
// query parameter. Validate refuses non-loopback binds without it.
AuthToken string
// AllowWrites opts in to admin actions from the dashboard (cancel
// pending/running tasks, rescue dead-lettered ones). ADR-0003's default
// is a read-only projection; writes are a conscious operator decision
// guarded by per-form CSRF tokens (withCSRF) and, on non-loopback
// binds, by the auth token. Off by default.
AllowWrites bool
// ReadModelPath, when set, moves the aggregate read side and the live
// notifications onto an ADR-0019 S3 metaengine read model: the
// projection at this path folds the journal (readmodel.PathFor
// derives it beside the queue db) and its Watcher replaces the hand
// tailer→hub fan-out. Status counts (nowband, /api/stats) read the
// collection; the task table, board, detail pages and fact feeds stay
// store-backed — the planned table does not carry lease owner,
// NotBefore or payload, which those row-rich views render. Empty =
// the store-backed defaults (off). Setting it requires Pump: this
// package never opens the projection itself (the composition root
// owns that engine — the single-opener seam).
ReadModelPath string
// Pump is the injected live-projection runtime consumed when
// ReadModelPath is set: the folded model plus its managed lifecycle
// (host start, teardown on Run's return). cmd/tq's serve wiring
// composes it so webui stays a leaf of the composition root rather
// than its peer. Nil with ReadModelPath set fails Validate; with
// ReadModelPath empty the hand tailer runs regardless.
Pump ProjectionPump
}
Config controls the dashboard server.
type DashboardData ¶
type DashboardData struct {
Counts map[task.Status]int
Total int
Tasks []task.Task
Dead []task.Task
Facts []journal.Fact
Filter FilterState
Now time.Time
Projects []ProjectSummary
Page int
TotalPages int
MatchTotal int
Budget *BudgetView
// Board holds the per-status columns when the filter selects the board
// view; nil on the table view (which uses Tasks/Page/TotalPages).
Board []BoardColumn
// JournalSeq is the journal watermark (highest fact seq) — the resume
// point every SSE/bridge consumer carries.
JournalSeq int64
// FactBuckets counts facts per equal slice of the last hour (the
// activity sparkline's series).
FactBuckets []float64
// CompleteMinutes is the time-to-complete (queue wait + run) of recent
// completed tasks in minutes, newest first (the completion histogram's
// raw data).
CompleteMinutes []float64
// Reviews holds the parsed verdict of every COMPLETED review task on
// the visible page (keyed by task id) — the agent-review loop made
// visible: an approve/request-changes badge in the table and findings
// on the detail page. Absent when the page shows no finished reviews.
Reviews map[string]executor.ReviewResult
// Parked is the rate-limit-parked count (pending with not_before in
// the future) — the "11 tasks parked until 19:40" one-glance segment
// in the nowband meta (16-00 report f44; the stats payload already
// carries the same number).
Parked int
// BudgetParked counts tasks parked by the claim-time budget gate:
// pending-with-not_before tasks whose latest task.requeued fact in
// the last 24h carries the budget class — the "the pool is idle ON
// PURPOSE (money)" lamp next to the parked segment. The 24h window
// is the badge's honest scope, not a live park state.
BudgetParked int
// Stranded counts tasks that can never run: pending with a dependency
// that is terminally not-completed (dead or cancelled). The claim
// gate skips them silently forever — the one stuck-middle state the
// operator can otherwise only find by reading dep graphs. Derived
// from existing reads (no stranded fact type; that contract addition
// awaits a demonstrated need), M14.
Stranded int
// LoopSuspects counts tasks whose claim count exceeds
// queue.ClaimAnomalyThreshold — the churn lamp in the nowband meta
// (the P1 class: one task out-claming its budget for days). tq stats
// lists the suspects; the webui only counts them.
LoopSuspects int
// SessionsOpened / SessionsClosed count the interactive-session
// lifecycle facts (session.opened / session.closed) — the session
// volume one-glance segment in the nowband meta (03-28 §f20).
SessionsOpened int
SessionsClosed int
// SessionsOpen is the begun-but-never-closed count — the lamp next to
// the volume segment (03-28 §f9/f20). Read-only observation: closing
// stays an explicit operator ruling.
SessionsOpen int
// AllowWrites mirrors Config.AllowWrites: the templates render the
// admin affordances (cancel/stop/rescue forms) only when writes are
// enabled server-side.
AllowWrites bool
// Statuses holds the parsed outcome of every COMPLETED status task on
// the visible page (keyed by task id) — the done-prompt loop made
// visible: a report badge in the table and the report path + next-item
// count on the detail page. Absent when the page shows no finished
// status reports.
Statuses map[string]executor.StatusResult
// Prioritizes holds the parsed outcome of every COMPLETED prioritize
// task on the visible page (keyed by task id) — the AI batch scorer
// made visible: a verdict-count badge in the table and the verdict
// count + derived session usage on the detail page. Nil when the page
// shows no finished scorer run.
Prioritizes map[string]executor.PrioritizeResult
// AgentResults holds the parsed outcome of every COMPLETED agent task
// on the visible page (keyed by task id) — the run's derived outcome
// (footer commits) + derived session usage made visible: a commit-count
// badge in the table and the result card on the detail page. Nil when
// the page shows no finished agent run.
AgentResults map[string]executor.AgentResult
}
DashboardData is the full projection snapshot one burst renders from.
type FilterState ¶
type FilterState struct {
Project string
Status task.Status
Query string
Page int
// Sort selects the task-table ordering: "", "age-asc", "age-desc",
// "priority-asc", "priority-desc", "attempts-asc", "attempts-desc".
// Empty keeps the severity order (dead, running, pending, ...).
Sort string
// View picks the task projection: viewTable (default) or viewBoard.
// Board drops Status (columns ARE the statuses) and ignores Sort/Page.
View string
// Band bounds the listing to one ADR-0015 band (backlog | hot |
// machine); empty means all bands.
Band string
}
FilterState is the URL-carried view filter (?project=&status=&q=). Page rides in the URL (?page=N) but is deliberately not part of QueryString: filter chips always reset to page 1.
func (FilterState) Empty ¶
func (f FilterState) Empty() bool
Empty reports whether no filter is active.
func (FilterState) QueryString ¶
func (f FilterState) QueryString() string
QueryString renders the filter as URL query parameters. Values are URL-escaped: a project or query containing &/=/space/% must round-trip through filterHref → parseFilter byte-identical (round-13 T5 armor).
type Hub ¶
type Hub struct {
// contains filtered or unexported fields
}
Hub fans out change notifications to all connected SSE clients. Notifications carry no payload — every one triggers a server-side re-render and a fresh full snapshot to each client (projection, not client state). The event ID is the journal watermark so browsers resume with a meaningful Last-Event-ID.
func (*Hub) Notify ¶
Notify broadcasts a change notification to every subscriber. seq is the journal watermark the notification represents.
func (*Hub) Unsubscribe ¶
Unsubscribe removes a subscriber channel.
type ProjectSummary ¶
ProjectSummary aggregates one project's live counts for the overview chips. Total is the project's task count across ALL statuses — the chip's headline number (R/P/D stay the operational breakdown).
type ProjectionPump ¶ added in v0.3.4
type ProjectionPump interface {
Model() *readmodel.Model
Run(ctx context.Context, notify func(cursor int64)) error
}
ProjectionPump is the live-projection surface the server consumes on the read-model path: the folded model backing the aggregate reads, driven by Run until the context is done. Run owns the pump's full lifecycle — start the managed host, fold the journal, notify with the applied journal cursor on every wake (the server's hub burst-coalesces exactly like the hand tailer's batch), and tear the runtime down before returning, so callers can drop their model references on defer.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the read-only live dashboard. Create with New, then call Run.
func New ¶
New creates a Server over the given store. The store is read-only from this package's perspective: only Get, List and Facts are called.
func (*Server) Handler ¶
Handler returns the dashboard's HTTP routes:
GET / dashboard page
GET /project/{name} dashboard pinned to one project
GET /task/{id} per-task detail page
GET /task/{id}/events per-task SSE stream (detail fragments + resume)
GET /api/events SSE stream (dashboard fragments + resume)
GET /api/stats JSON status counts
GET /api/facts JSON journal cursor (?after=SEQ&limit=N)
The go-health-dashboard surface (healthBindings) mounts on top: /health, /health/sse, /health/datastar.js, /health/favicon.svg, and the JSON probes /healthz, /readyz, /startupz — all read-only, all behind the same auth gate; the /health page runs the dashboard's own CSP (health.go).
With Config.AllowWrites the admin routes are added on top:
POST /task/{id}/cancel withdraw a pending task / request a running stop
POST /task/{id}/rescue re-queue a dead-lettered task (fresh attempts)
Every response carries strict security headers (securityHeaders); the default dashboard is read-only by construction, writes are opt-in and CSRF-guarded (withCSRF).