webui

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2026 License: MIT Imports: 33 Imported by: 0

Documentation

Overview

templ: version: v0.3.1020

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 (
	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) 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.

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 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
	// 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
}

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
	// 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
}

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
}

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.

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) ClientCount

func (h *Hub) ClientCount() int

ClientCount returns the number of connected subscribers.

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
	Pending int
	Running int
	Dead    int
}

ProjectSummary aggregates one project's live counts for the overview chips.

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)

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