webui

package
v0.3.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 42 Imported by: 0

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

View Source
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.

View Source
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

View Source
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

func TaskNotFoundPage(id string) templ.Component

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

type BoardBandGroup struct {
	Band  queue.Band
	Tasks []task.Task
}

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

type BoardColumn struct {
	Status    task.Status
	Count     int
	Tasks     []task.Task
	Truncated int
}

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

type BudgetView struct {
	Cap   int
	Spent int
}

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).
	ReadModelPath string
}

Config controls the dashboard server.

func (Config) Validate

func (c Config) Validate() error

Validate rejects configurations that would expose the unauthenticated dashboard beyond the loopback interface: an address whose host part binds non-loopback interfaces requires AuthToken. The zero Config (loopback default, no token) stays valid.

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 NewHub

func NewHub() *Hub

NewHub creates a ready-to-use Hub.

func (*Hub) Notify

func (h *Hub) Notify(seq int64)

Notify broadcasts a change notification to every subscriber. seq is the journal watermark the notification represents.

func (*Hub) Shutdown

func (h *Hub) Shutdown(ctx context.Context) error

Shutdown drains the broadcaster.

func (*Hub) Subscribe

func (h *Hub) Subscribe() <-chan sse.Event

Subscribe returns a channel receiving notifications.

func (*Hub) Unsubscribe

func (h *Hub) Unsubscribe(ch <-chan sse.Event)

Unsubscribe removes a subscriber channel.

type ProjectSummary

type ProjectSummary struct {
	Name    string
	Total   int
	Pending int
	Running int
	Dead    int
}

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 Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the read-only live dashboard. Create with New, then call Run.

func New

func New(store queue.Store, cfg Config) *Server

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

func (s *Server) Handler() http.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).

func (*Server) Run

func (s *Server) Run(ctx context.Context) error

Run starts the read model (when configured) and the journal tailer, and serves until ctx is cancelled or the listener fails. It always shuts the HTTP server down gracefully. It refuses to start on a non-loopback bind without a token (Validate).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL