webui

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package webui serves deploy's local-first operator interface and is the seam where an embedding product supplies its own branding.

The package owns presentation only: the embedded templates and assets, the page and asset handlers, and the read-only snapshot endpoint. Agent execution, approvals, event publication, and fleet state belong to the containing core, which supplies them through ISnapshotProvider — the single input this package takes — and which mounts its own action endpoints and event stream alongside this handler.

Presentation is the package's one configurable policy, and it is configured in three widening steps. WithBrand replaces the product name, the agent name, the wordmark, and the design tokens; every other string in the UI stays literal. WithNavItem appends entries to the sidebar. WithUIOverlay resolves templates and assets against a caller-supplied filesystem before the embedded one. The palette is delivered as a served same-origin stylesheet rather than an inline style block, so the page's Content-Security-Policy stays 'self' for both styles and scripts no matter how far a caller goes.

Template override points

An overlay template file contributes {{define}} blocks into the same namespace the embedded files use, so redefining one of the names below replaces exactly that much of the UI. These are the supported names, the whole of them, and the data each one receives:

"index.html"     the operator home page; receives the page data below
"chat.html"      the live session page; receives the page data below
"primaryNav"     the sidebar navigation list; receives the bound
                 navigation items, each carrying the NavItem fields
                 (Label, Href, Glyph, View) plus Active, Count, and
                 CountAttribute
"statusPill"     one status chip; receives the status string
"browserRoutes"  the body's data-route-* attributes; receives the route
                 table the browser client reads its URLs from
"browserEnums"   the body's data-enum-* attributes; receives the enum
                 table the browser client compares against

The two page blocks receive .Brand, .Snapshot, .Nav, .Routes, .Enums, .InitialJSON, .Unavailable, and .ChatSessionID. Every template helper the shipped blocks use is available to an override of them.

Two of these names are load-bearing beyond their markup. "browserRoutes" and "browserEnums" are how the browser client learns its URLs and enum spellings; an override that drops an attribute silently disables the behavior behind it. The embedded client also reads the navigation back out of the rendered DOM, so an overridden "primaryNav" keeps in-page view switching as long as its entries carry data-nav values naming sections the page rendered.

Worked examples

Two runnable programs in this module compose these options through the composition root, at both ends of the range:

examples/custom-ui-page   the stock identity, one sidebar entry, and the
                          page behind it — the smallest useful extension
examples/custom-brand     a full rebrand: both brand-bearing names, a
                          wordmark drawn around a glyph its overlay adds, a
                          replaced palette, one sidebar entry, and its page

Each carries a hermetic suite that renders the pages without the infrastructure an assembled Core opens.

Callers may rely on New failing rather than returning a handler without a snapshot source, on Handler being safe for concurrent use once New returns, on MarshalSnapshot producing the one canonical JSON encoding the embedded browser client parses, on every URL the rendered pages emit coming from browserroutes rather than a literal in a template, on the asset URL space and its cache headers being the same for overlay and embedded files alike, and on an unconfigured handler rendering exactly the stock deploy identity.

Index

Constants

View Source
const (
	DefaultProductName = "Candace Deploy"
	DefaultAgentName   = "Claw"
)

DefaultProductName and DefaultAgentName are the stock deploy identity. They are the fallbacks the browser client also carries, so a page rendered without an explicit brand reads exactly as it always has.

View Source
const (
	NavViewHome     = "home"
	NavViewApps     = "apps"
	NavViewFleet    = "fleet"
	NavViewActivity = "activity"
)

The built-in view names. Each names one <section data-view="…"> the shipped index page renders, and one sidebar entry that switches to it in place rather than navigating. They are exported so an embedding product can point an extra entry at a view it already knows, and so a test can name one without copying a string literal.

View Source
const DefaultWordmark = template.HTML(
	`<span class="brand-mark" aria-hidden="true"><span></span><span></span><span></span></span>` +
		`<span>Candace<span class="brand-accent">Deploy</span></span>`,
)

DefaultWordmark is the stock deploy lockup: the rotated diamond mark followed by the two-tone product name. Its classes are styled by the embedded app.css.

Variables

View Source
var ErrInvalidBrandName = errors.New("webui: invalid brand name")

ErrInvalidBrandName reports a product or agent name that is not renderable plain text.

View Source
var ErrInvalidNavItem = errors.New("webui: invalid navigation item")

ErrInvalidNavItem reports a navigation item that cannot be rendered as one labeled sidebar link.

View Source
var ErrInvalidPaletteValue = errors.New("webui: invalid palette value")

ErrInvalidPaletteValue reports a palette entry that is not a safe CSS token value. The wrapped message names the token and what disqualified it.

View Source
var ErrInvalidUIOverlay = errors.New("webui: invalid UI overlay")

ErrInvalidUIOverlay reports an overlay that cannot supply presentation: a missing filesystem, a second overlay, or a template file that does not parse.

View Source
var ErrNilSnapshotProvider = errors.New("webui: nil snapshot provider")

ErrNilSnapshotProvider is returned when the UI is constructed without a source of immutable display state.

Functions

func MarshalSnapshot

func MarshalSnapshot(snapshot *deployv1.WebUISnapshot) ([]byte, error)

MarshalSnapshot preserves the compact browser JSON contract while using the canonical generated message as its only state model. Snapshots reach it already carrying the producing core's brand; the stock names fill only the fields a producer left empty.

Types

type Brand

type Brand struct {
	// ProductName is the system's name — the page title, the operator-facing
	// subject of the UI's sentences, and the snapshot's System.Name default.
	ProductName string

	// AgentName is the name of the agent that acts for the operator.
	AgentName string

	// Wordmark is the lockup markup rendered inside the sidebar and chat brand
	// links.
	//
	// SECURITY: this value is operator-trusted markup. It is emitted into the
	// page verbatim, without escaping, exactly like a template the operator
	// wrote. Never populate it from a browser request, a fleet node, an agent,
	// or any other untrusted source; a hostile fragment can carry arbitrary
	// markup. It cannot smuggle script past the page's Content-Security-Policy,
	// which permits only same-origin scripts, but it can still restyle or
	// deface the shell. Supply a fragment reviewed in the same way as the
	// embedded templates.
	//
	// An unset Wordmark falls back to the stock lockup when no ProductName was
	// supplied either, and to the escaped ProductName otherwise, so naming a
	// product without drawing one never leaves the shell wearing another
	// product's mark.
	Wordmark template.HTML

	// Palette overrides the stylesheet's :root design tokens. Unset entries
	// keep the embedded app.css value.
	Palette Palette
}

Brand is the identity this package renders. It is the whole of what an embedding product replaces: the two brand-bearing strings, the lockup markup, and the design tokens the stylesheet reads.

The zero Brand is the stock deploy identity: every unset field falls back to its DefaultBrand value, so a caller may name only what it wants to change.

func DefaultBrand

func DefaultBrand() Brand

DefaultBrand returns the stock deploy identity: the shipped names, the shipped wordmark, and no palette overrides.

func (Brand) Resolved

func (b Brand) Resolved() Brand

Resolved returns the brand with every unset field filled from DefaultBrand and the names trimmed. Composition roots that hold one Brand and hand it to several packages should resolve it once, so each of them renders the same identity rather than repeating the fallbacks.

func (Brand) Validate

func (b Brand) Validate() error

Validate reports whether the supplied fields are usable. Unset fields are valid: they resolve to their DefaultBrand values.

type Handler

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

Handler serves the embedded UI. It is safe for concurrent use after New returns.

func New

func New(provider ISnapshotProvider, functionalOptions ...Option) (*Handler, error)

New parses the embedded pages and constructs a handler for pages, static assets, and the read-only snapshot API. The containing core should mount its POST action endpoints and GET /api/events alongside this handler.

Without options the handler serves the stock deploy identity.

func (*Handler) HandleAsset

func (h *Handler) HandleAsset(w http.ResponseWriter, r *http.Request)

HandleAsset serves an embedded browser asset with cache and content headers. The brand stylesheet is generated rather than embedded, so it is answered here instead of from the embedded filesystem.

func (*Handler) HandleChat

func (h *Handler) HandleChat(w http.ResponseWriter, r *http.Request, sessionID string)

HandleChat renders the current run only when it belongs to sessionID.

func (*Handler) HandleIndex

func (h *Handler) HandleIndex(w http.ResponseWriter, r *http.Request)

HandleIndex renders the operator home page.

func (*Handler) HandleSnapshot

func (h *Handler) HandleSnapshot(w http.ResponseWriter, r *http.Request)

HandleSnapshot writes the read-only browser snapshot.

func (*Handler) Register

func (h *Handler) Register(router gin.IRouter)

Register mounts the presentation routes on a caller-owned Gin router.

type ISnapshotProvider

type ISnapshotProvider interface {
	Snapshot(ctx context.Context) (*deployv1.WebUISnapshot, error)
}

ISnapshotProvider supplies one immutable, internally consistent view of the core. Implementations should honor cancellation from the request context.

type NavItem struct {
	// Label is the visible text of the link. It is required and is escaped
	// like any other page text.
	Label string

	// Href is the link target. It is required. A relative or same-document
	// target ("#apps", "/reports") is the normal case; the template's URL
	// filter neutralizes a scheme the browser must not follow.
	Href string

	// Glyph is the decorative mark rendered before the label. It is optional
	// and aria-hidden, so it carries no information the label does not.
	Glyph string

	// View optionally names an in-page section rendered as
	// <section data-view="…">. When it names one, the browser client switches
	// to that section in place and marks this entry current; when it is empty
	// or names no rendered section, the link is ordinary navigation. Only
	// letters, digits, hyphens, and underscores are accepted, so the name
	// stays usable as both a URL fragment and an attribute value.
	View string
}

NavItem is one entry in the operator sidebar's primary navigation.

The shipped entries are ordinary NavItems: WithNavItem appends to the same slice the defaults occupy, so a registered entry renders with the same markup, the same keyboard behavior, and the same aria semantics as Home, Apps, Fleet, and Activity.

func DefaultNavItems

func DefaultNavItems() []NavItem

DefaultNavItems returns the shipped sidebar entries in render order: the four built-in views with their glyphs. Callers that want to reorder or drop an entry override the "primaryNav" template through WithUIOverlay; WithNavItem only appends.

func (n NavItem) Validate() error

Validate reports whether the item can be rendered as one sidebar link.

type Option

type Option func(settings *presentationOptions) error

Option changes one public presentation policy.

func WithBrand

func WithBrand(brand Brand) Option

WithBrand replaces the stock deploy identity. Unset Brand fields keep their stock values, and an invalid brand fails New rather than rendering a half-branded page.

func WithNavItem

func WithNavItem(item NavItem) Option

WithNavItem appends one entry to the operator sidebar's primary navigation, after the shipped Home, Apps, Fleet, and Activity entries. It is repeatable; entries render in registration order. An invalid item fails New rather than rendering a broken link.

A registered entry is a plain link: it carries no live count, and unless its View names a section the page actually renders, following it is ordinary navigation rather than the in-page view switch the built-in entries perform.

func WithUIOverlay

func WithUIOverlay(overlay fs.FS) Option

WithUIOverlay resolves presentation against a caller-supplied filesystem before the embedded one. The overlay is a single tree with the same shape as this package's own:

templates/*.html   named-block definitions that redefine the shipped ones
assets/*           files served in place of the embedded asset of that name

Both subtrees are optional and both are overlay-first with embedded fallback: a name the overlay does not carry keeps shipping from the embedded filesystem, so an overlay replaces only what it names.

An overlay template file contributes {{define}} blocks; its text outside a define is ignored. Replacing a whole page therefore means defining that page's block, exactly as the embedded files do. The supported block names, and the data each receives, are listed in this package's documentation.

Overlay assets are served by the same handler as the embedded ones and keep its behavior: the same asset URL space, the same cache and content-type headers, and the same generated brand stylesheet layered on top. An overlay file outside assets/ is never reachable through the asset route.

Only one overlay may be supplied. Overlay templates are operator-trusted markup on the same footing as Brand.Wordmark: they are page source, not escaped content, so never assemble one from a browser request, a fleet node, or an agent. The page's Content-Security-Policy is unchanged and still permits only same-origin styles and scripts, so an overlay that inlines either will simply not run.

type Palette

type Palette struct {
	// Surfaces.
	Canvas     string // --canvas
	CanvasDeep string // --canvas-deep
	Card       string // --card
	CardStrong string // --card-strong

	// Text.
	Ink     string // --ink
	InkSoft string // --ink-soft
	Muted   string // --muted
	Faint   string // --faint

	// SidebarInk is the text color of the operator sidebar, which sits on
	// Forest rather than on Canvas and therefore does not follow Ink.
	SidebarInk string // --sidebar-ink

	// Rules and separators.
	Line       string // --line
	LineStrong string // --line-strong

	// Accents.
	Forest      string // --forest
	ForestLight string // --forest-light
	Mint        string // --mint
	MintStrong  string // --mint-strong

	// BrandAccent is the accent the stock wordmark paints its second half
	// with. A Wordmark that does not use the shipped markup never renders it.
	BrandAccent string // --brand-accent

	// Status colors.
	Green           string // --green
	Amber           string // --amber
	AmberBackground string // --amber-bg
	AmberLine       string // --amber-line
	Red             string // --red
	RedBackground   string // --red-bg
	Blue            string // --blue

	// Elevation, shape, and type.
	Shadow      string // --shadow
	ShadowSmall string // --shadow-small
	Radius      string // --radius
	RadiusSmall string // --radius-small
	Mono        string // --mono
}

Palette overrides the design tokens the embedded stylesheet declares on :root. Every field is one CSS custom property; an empty field keeps the shipped value.

Values are validated rather than escaped, because a custom property value is substituted into the stylesheet as CSS rather than as text. Validate rejects anything that could end the declaration, end the rule, open a comment, or reference an external resource, so one token cannot smuggle a second declaration or a network fetch into the page.

func (Palette) Stylesheet

func (p Palette) Stylesheet() string

Stylesheet renders the configured tokens as one :root rule. It returns the empty string when no token is set, which is what the stock brand serves: the page still links the stylesheet, and the embedded app.css values stand.

Callers must Validate first; Stylesheet renders whatever it is given.

func (Palette) Validate

func (p Palette) Validate() error

Validate reports every entry that is not a safe CSS token value. An empty palette, and every empty entry within a palette, is valid.

type SnapshotFunc

type SnapshotFunc func(ctx context.Context) (*deployv1.WebUISnapshot, error)

SnapshotFunc adapts a function to ISnapshotProvider.

func (SnapshotFunc) Snapshot

Snapshot implements ISnapshotProvider.

Jump to

Keyboard shortcuts

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