web

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package web serves the mAPI-ng dashboard: the fixed, non-configurable, auto- generated 3-level RED view (CONTEXT Dashboard) plus the live 4-step onboarding panel driven by the handshake. It is server-rendered HTML (html/template autoescaping) with no client-side JavaScript framework: a strict script-src 'self' CSP (ADR-0008) rules out CDN scripts, so the time-series and latency histogram charts are inline server-rendered SVG (see chart.go). The only scripts served are self-hosted assets/copy.js (copy-to-clipboard) and assets/handshake.js (in-place polling of the onboarding stepper via GET /setup/handshake, replacing the old full-page meta-refresh). The /api/series and /api/histogram JSON endpoints remain in the code but are not consumed by the current UI.

The three levels are:

  1. GET / service overview (rate/error%/p50/p95/p99 per service)
  2. GET /services/{service} endpoint table, server-side sortable via ?sort=
  3. GET /services/{service}/endpoint?method=&route= endpoint detail + histogram

The active tenant is resolved PER REQUEST via an injected func, never hardcoded: for Part 1 main supplies a constant dev-tenant func, and Part 2 (auth) will swap in the authenticated org — this is the seam that keeps auth out of Part 1. The onboarding source and cardinality-frozen check are also injected and nil-safe, so the dashboard still renders when there is no control plane (dev-without-Postgres).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	Querier    Querier
	Tenant     TenantResolver
	Onboarding OnboardingSource
	Frozen     FrozenFunc
	// KeyAdmin drives the self-serve Setup keys panel. Nil (dev/no-control-plane)
	// hides the panel and 404s the key POSTs.
	KeyAdmin KeyAdmin
	// MemberAdmin drives the self-serve Setup team panel (members + invites). Nil
	// (dev/no-control-plane) hides the panel and 404s its POSTs.
	MemberAdmin MemberAdmin
	// Role resolves the caller's role per request, so the team panel can admin-gate
	// its create/revoke/remove actions. Nil is treated as "not an admin".
	Role RoleResolver
	// CSRFKey signs the Setup form CSRF tokens (HMAC). Required (>= 1 byte) when
	// KeyAdmin or MemberAdmin is set; ignored otherwise. main passes the session key.
	CSRFKey []byte
	Logger  *slog.Logger
	// Sidebar identity chrome (display only). Empty values fall back to
	// sensible defaults so the dashboard renders without a control plane.
	OrgName  string
	UserName string
	UserRole string
	// AccountHref, when non-empty, turns the sidebar user-identity block into a link
	// to that path — a composing build (via app.WithAccountLink) points it at the
	// account page it owns. Empty leaves the block a non-interactive display element.
	AccountHref string
}

Config bundles the Handler dependencies so NewHandler's signature stays readable as the injected surface grows (tenant, onboarding, frozen).

type FrozenFunc

type FrozenFunc func(tenant string) bool

FrozenFunc reports whether a tenant's cardinality is frozen on this node, so the onboarding/dashboard can surface the guardrail warning loudly (CONTEXT Guardrails). Nil-safe: unset means "no frozen signal available", not "false".

type Handler

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

Handler serves the 3-level dashboard, the onboarding panel, and the JSON data endpoints. Every dependency beyond the querier is injected and nil-safe.

func NewHandler

func NewHandler(cfg Config) (*Handler, error)

NewHandler builds the dashboard Handler. Querier and Tenant are required; Onboarding and Frozen are optional (nil-safe) so the dashboard renders without a control plane or guardrail signal.

func (*Handler) Register

func (h *Handler) Register(mux *http.ServeMux)

Register mounts the dashboard routes on mux. /dashboard aliases / so both URLs resolve to the overview.

func (*Handler) RenderShellPage

func (h *Handler) RenderShellPage(w http.ResponseWriter, r *http.Request, title string, content template.HTML)

RenderShellPage renders inner content wrapped in the full dashboard chrome (sidebar + top bar), so a composing build can present a page it owns — e.g. an account page — that looks native to the dashboard rather than a detached page. content is trusted HTML the caller has already escaped/produced from a template; title sets the top-bar heading and the browser tab.

type InviteInfo

type InviteInfo struct {
	ID        string
	Email     string
	Role      string
	CreatedAt time.Time
	ExpiresAt time.Time
}

InviteInfo is a listed pending invite for the Setup team panel. It mirrors control.InviteInfo (the secret token is never surfaced here, only its metadata).

type KeyAdmin

type KeyAdmin interface {
	IssueKey(ctx context.Context, orgID, label string) (token string, err error)
	ListKeys(ctx context.Context, orgID string) ([]KeyInfo, error)
	RevokeKey(ctx context.Context, orgID, keyID string) error
}

KeyAdmin is the self-serve key surface the Setup page drives: issue (returns the full one-time token, origin already wrapped by main), list, and revoke. Nil-safe: when unset (dev/no-control-plane) the keys panel is hidden and the key POST routes 404, so the dashboard still renders without a control plane.

type KeyInfo

type KeyInfo struct {
	ID        string
	Label     string
	Last4     string
	CreatedAt time.Time
	RevokedAt *time.Time // nil while the key is active
}

KeyInfo is a listed ingest key for the Setup keys panel. It mirrors control.KeyInfo so the web layer never imports the control plane; main adapts between the two. It never carries the secret — only the display last-4 and lifecycle timestamps.

type MemberAdmin

type MemberAdmin interface {
	ListMembers(ctx context.Context, orgID string) ([]MemberInfo, error)
	ListInvites(ctx context.Context, orgID string) ([]InviteInfo, error)
	CreateInvite(ctx context.Context, orgID, invitedBy, email, role string) (link string, err error)
	RevokeInvite(ctx context.Context, orgID, inviteID string) error
	RemoveMember(ctx context.Context, orgID, memberID string) error
	SeatUsage(ctx context.Context, orgID string) (used, limit int, err error)
}

MemberAdmin is the self-serve team surface the Setup page drives: list members and pending invites, create an invite (returns the full one-time accept link, origin already wrapped by main), revoke an invite, remove a member, and read seat usage. Nil-safe: when unset (dev/no-control-plane) the team panel is hidden and its POST routes 404. Its state-changing actions are admin-gated at the handler (see Role).

type MemberInfo

type MemberInfo struct {
	ID        string
	Email     string
	Role      string
	CreatedAt time.Time
	IsOwner   bool
}

MemberInfo is a listed org member for the Setup team panel. It mirrors control.MemberInfo so the web layer never imports the control plane.

type OnboardingSource

type OnboardingSource func(ctx context.Context, tenant string) ([]ServiceOnboarding, error)

OnboardingSource returns the connected services for a tenant, driving the onboarding panel. Nil-safe: when unset (no control plane), the panel shows the key-valid step and nothing beyond it rather than inventing data.

type Querier

type Querier interface {
	Tenant(id tenant.ID) ScopedQuery
}

Querier is the read side the web layer depends on. It exposes no un-scoped query: the only way to reach the data plane is Tenant(tenant), which returns a tenant-bound ScopedQuery. This makes a cross-tenant read unrepresentable — isolation is a type property, not caller discipline. storage.TenantQuery structurally satisfies ScopedQuery; a fake satisfies Querier in tests, so the web layer never imports a live ClickHouse connection.

type RoleResolver

type RoleResolver func(r *http.Request) (role, memberID string, ok bool)

RoleResolver reports the caller's role (and member id) for the active request, so the team panel can admin-gate its actions. ok=false (no control plane / no session) is treated as "not an admin". main supplies it from the auth session.

type ScopedQuery

type ScopedQuery interface {
	SeriesOverTime(ctx context.Context, service, method, route string, from, to time.Time, step time.Duration) ([]storage.TimePoint, error)
	Services(ctx context.Context, from, to time.Time) ([]storage.ServiceStat, error)
	Endpoints(ctx context.Context, service string, from, to time.Time) ([]storage.EndpointStat, error)
	EndpointDetail(ctx context.Context, service, method, route string, from, to time.Time) (storage.EndpointDetail, error)
	InstancesForEndpoint(ctx context.Context, service, method, route string, from, to time.Time) ([]storage.InstanceStat, error)
	VersionsForEndpoint(ctx context.Context, service, method, route string, from, to time.Time) ([]storage.VersionStat, error)
	ExemplarsForEndpoint(ctx context.Context, service, method, route string, from, to time.Time) ([]storage.ExemplarRow, error)
	LatencyByStatusClass(ctx context.Context, service, method, route string, from, to time.Time) (map[string]storage.ClassLatency, error)
	ErrorClassesForEndpoint(ctx context.Context, service, method, route string, from, to time.Time) ([]storage.ErrorClassStat, error)
	NoStatusReasonsForEndpoint(ctx context.Context, service, method, route string, from, to time.Time) ([]storage.NoStatusReasonStat, error)
	DownstreamForEndpoint(ctx context.Context, service, method, route string, from, to time.Time) (storage.DownstreamStat, error)
	InstanceResourcesForService(ctx context.Context, service string, from, to time.Time) ([]storage.InstanceResourceStat, error)
	MemoryTrendForService(ctx context.Context, service string, from, to time.Time, step time.Duration) ([]storage.MemoryTrendPoint, error)
	PerformanceStats(ctx context.Context, from, to time.Time) (storage.PerformanceStat, error)
	HasAnySummary(ctx context.Context) (bool, error)
}

ScopedQuery is the tenant-bound aggregate surface the dashboard reads. Every method is already scoped to the tenant the handle was created for, so no call site passes a tenant string.

type ServiceOnboarding

type ServiceOnboarding struct {
	Service     string
	Instance    string
	HandshakeAt time.Time
}

ServiceOnboarding mirrors control.ServiceOnboarding without importing control (web sits downstream of the control plane and must not depend on it). main adapts the control type into this at wiring time.

type Shell

type Shell struct {
	Org, User, Role string
	// AccountHref, when non-empty, makes the sidebar user-identity block a link to
	// the composing build's account page. Empty leaves it a display-only element.
	AccountHref  string
	Nav          []navItem
	Crumbs       []crumb
	PageTitle    string
	ShowControls bool
	Windows      []windowOption
	WindowKey    string
	FlushLabel   string
	// KeyMask is the masked last-4 ("····<last4>") of the tenant's newest active
	// ingest key, shown in the sidebar. Empty when there is no control plane or no
	// active key, so the sidebar shows a muted "no active key" instead of a fake.
	KeyMask string
	// Live drives the casual-check auto-refresh: when true the page emits a
	// meta-refresh and the top-bar indicator reads "live". LiveHref is the toggle
	// link (set only on pages that support live refresh — the overview); an empty
	// LiveHref renders the static, non-clickable indicator.
	Live     bool
	LiveHref string
}

Shell is the chrome shared by every page: the sidebar identity + nav and the top bar (breadcrumbs, title, window switcher). Each page's view data embeds a Shell so the shared sidebar/topbar sub-templates render from one place.

type TenantResolver

type TenantResolver func(r *http.Request) (id tenant.ID, ok bool)

TenantResolver resolves the active tenant for a request. Part 1 supplies a constant-tenant func; Part 2 (auth) supplies the authenticated org. ok=false means no tenant could be resolved (unauthenticated), which the handlers turn into a 401 — but Part 1's constant func always returns ok=true.

Jump to

Keyboard shortcuts

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