# Status Indicators A status page that shows a colored dot per monitored service ("api", "database", "email", "cdn"). A health-check process (a cron job, a webhook handler, an admin form) calls a `setStatus` rpc; the rpc broadcasts on a `Channel`. Every browser on the status page receives the broadcast and re-colors the dot for the matching service. No table, no LiveTable. The latest broadcast wins, and the page reflects it. This is a pure-channel pattern. Status is ephemeral state: clients care about the current value, not the history. The recipe lives entirely in one page directory plus the channel definition. ## Page setup ```bash elements create page status ``` The service names, the status union, the channel, and the `setStatus` rpc are used only by the `/status` page (and whatever external triggers call the rpc), so they live in the page's own `services.ts`. The route loads them and hands a listener to the template. `app/pages/status/services.ts`: ```ts import { Channel } from "@elements/app"; export type ServiceName = "api" | "database" | "email" | "cdn"; export type Status = "ok" | "degraded" | "down" | "unknown"; export interface StatusEvent { name: ServiceName; status: Status; } export const SERVICES: ServiceName[] = ["api", "database", "email", "cdn"]; export const statusChannel = new Channel("service-status"); /** @rpc */ export function setStatus(name: ServiceName, status: Status) { statusChannel.notify({ name, status }); } ``` The `SERVICES` constant is the list of monitored services. The channel carries `StatusEvent` payloads: a service name and its new status. `setStatus` is the only rpc; it's the universal entry point that whatever runs health checks (a cron, a webhook, manual admin action) calls when a status changes. `app/pages/status/index.ts`: ```ts import status from "./template"; import { statusChannel, SERVICES } from "./services"; export default function route(req, res) { return new status({ services: SERVICES, listener: statusChannel.listen(), }); } ``` The route returns the list of services plus a listener attached to the status channel. The first paint shows every service with `unknown` status; the listener updates them in place as broadcasts arrive. `app/pages/status/template.html`: ```html import "./style.css"; import type { Listener } from "@elements/app"; import { ServiceName, Status, StatusEvent } from "./services"; function onIncoming(event: StatusEvent, statuses: { value: Record }) { statuses.value = { ...statuses.value, [event.name]: event.status }; } function statusFor(name: ServiceName, statuses: Record): Status { return statuses[name] ?? "unknown"; } , private statuses: { value: Record } = { value: {} as Record }) oninit={() => listener.on("notify", (e) => onIncoming(e, statuses))}>

service status

  • {name} {statusFor(name, statuses.value)}
``` `statuses.value` is a `Record`. `onIncoming` reassigns the whole object (`{ ...statuses.value, [name]: status }`) so the binding re-runs and the row's class flips. The `class={"service status-" + statusFor(...)}` interpolation maps the current status to a CSS class (`status-ok`, `status-degraded`, `status-down`, `status-unknown`), which is where the dot color lives. `statusFor` falls back to `"unknown"` for services without a broadcast yet. That's the initial state on every fresh page load. `app/pages/status/style.css`: ```css @import "#app/shared/styles/page.css"; .status ul.services { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--space-3); max-width: var(--container-prose); } .status .service { display: grid; grid-template-columns: 12px 1fr auto; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border: 1px solid var(--rule); border-radius: var(--radius-md); } .status .dot { width: 12px; height: 12px; border-radius: var(--radius-full); background: var(--ink-faint); } .status .status-ok .dot { background: var(--green); } .status .status-degraded .dot { background: var(--yellow); } .status .status-down .dot { background: var(--red); } .status .name { font-weight: var(--font-medium); } .status .state { color: var(--ink-muted); font-size: var(--text-sm); } ``` The dot color comes from the `status-*` class on the parent `
  • `. Adding a new status (`"maintenance"`) means three changes: the `Status` union, a new `.status-maintenance .dot` rule, and any UI text that lists the legend. ## Routes Register the page in `index.ts`: ```ts import status from "#app/pages/status"; // ... app.route("/status", status); ``` ## Notes - **Initial state is "unknown".** On a fresh page load, no broadcasts have arrived yet, so every service renders as gray. The first health-check tick populates the real values. For a friendlier first paint, persist the latest status per service in a `serviceStatuses` table; the route reads it for initial render, the channel still drives live updates. - **Triggers for `setStatus`.** Anything server-side can call it: an `app.cron` health-check loop, a webhook from your monitoring vendor (a `POST /webhook/pagerduty` route that parses the body and calls `setStatus(...)`), an admin button in another page. The recipe stays decoupled from the trigger. - **Cron health check.** Add an `app.cron("every 30s", "check services", () => new HealthCheckJob().schedule())` and a `HealthCheckJob.run()` that pings each service (or hits an internal endpoint) and calls `setStatus`. The page picks up every change through the channel without any extra wiring. - **Per-tenant status pages.** Add `tenantId` to `StatusEvent` and filter the listener: `statusChannel.listen({ filter: (e) => e.tenantId === currentTenantId })`. The same broadcast channel handles every tenant; the filter splits the stream. - **Why no LiveTable.** LiveTable broadcasts on row insert/update/delete. Status events have no row identity to track (the latest value replaces the previous one, there is no history). The channel matches what the recipe needs: notify the new value, listen, replace local state.