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:
- GET / service overview (rate/error%/p50/p95/p99 per service)
- GET /services/{service} endpoint table, server-side sortable via ?sort=
- 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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
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 ¶
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.