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
- Variables
- func MarshalSnapshot(snapshot *deployv1.WebUISnapshot) ([]byte, error)
- type Brand
- type Handler
- func (h *Handler) HandleAsset(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleChat(w http.ResponseWriter, r *http.Request, sessionID string)
- func (h *Handler) HandleIndex(w http.ResponseWriter, r *http.Request)
- func (h *Handler) HandleSnapshot(w http.ResponseWriter, r *http.Request)
- func (h *Handler) Register(router gin.IRouter)
- type ISnapshotProvider
- type NavItem
- type Option
- type Palette
- type SnapshotFunc
Constants ¶
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.
const ( )
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.
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 ¶
var ErrInvalidBrandName = errors.New("webui: invalid brand name")
ErrInvalidBrandName reports a product or agent name that is not renderable plain text.
ErrInvalidNavItem reports a navigation item that cannot be rendered as one labeled sidebar link.
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.
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.
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.
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 ¶
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.
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 ¶
type NavItem struct {
// like any other page text.
Label string
// target ("#apps", "/reports") is the normal case; the template's URL
// filter neutralizes a scheme the browser must not follow.
Href string
// and aria-hidden, so it carries no information the label does not.
Glyph string
// <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.
type Option ¶
type Option func(settings *presentationOptions) error
Option changes one public presentation policy.
func WithBrand ¶
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 ¶
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 ¶
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 ¶
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.
type SnapshotFunc ¶
type SnapshotFunc func(ctx context.Context) (*deployv1.WebUISnapshot, error)
SnapshotFunc adapts a function to ISnapshotProvider.
func (SnapshotFunc) Snapshot ¶
func (f SnapshotFunc) Snapshot(ctx context.Context) (*deployv1.WebUISnapshot, error)
Snapshot implements ISnapshotProvider.