Wheelhouse docs

State, intent and drift#

A router has two truths. Intent is the configuration tree: what somebody declared. State is what the kernel and the daemons are doing right now: which links are up, which routes are in the forwarding table, which qdisc is attached, which containers are running. Where the two disagree, this documentation calls the disagreement drift.

Every screen in Wheelhouse shows both, in visually distinct forms, and names the disagreement where it finds one.

Reading a page#

  • A plain field is something the configuration says.
  • A value in monospace with a pulse dot beside it, or in a counter column, came off the router just now.
  • A drift badge means the two do not match, and the badge says how.

The pulse is the only animation in the product. It means the number is arriving rather than remembered, and it is disabled under prefers-reduced-motion.

Where the comparison is made#

ScreenIntentStateWhat drift looks like
Interfacesinterfaces … in the config treeshow interfaces <kind> <name>: link state, MTU, addresses, MAC, qdisc, RX/TX countersAn interface the router reports but the configuration does not declare gets a badge reading present on the router but not in the config tree
Static routesprotocols static …the kernel forwarding tables, from show ip route and show ipv6 routeEach route is Installed or Not installed, and the Static routes tile counts the ones not installed
QoSthe qos policy attached to an interfacethe qdisc the kernel installed, read off the interfaceGET /api/qos/stats returns {policy, qdisc, rx, tx} per shaped interface
Appscontainer name …the router's own container list and a reachability probeDeclared but not running, running but not declared, and not probed where the agent runs off-router
High availabilityhigh-availability …keepalived's per-group state from show vrrprunning: false when the router says the data is not available, rather than an empty table
IPsec, OpenVPNthe vpn ipsec / interfaces openvpn configurationthe associations and the per-mode status text the router printsThe same running: false treatment
Reconcilethe desired-state filethe whole running configurationGET /api/drift — a different kind of drift; see below

Nothing reconciles in the background and reports success. Where Wheelhouse notices a difference it puts it on the page, and where it can act on one it hands the operations to the Commit Bar to be committed by a person.

Two things called drift#

Per-object drift is state against intent, on one screen, as in the table above. It is computed in the browser or in a handler at the moment you look, from a read of the router. Nothing is stored.

Desired-state drift is the running configuration against a file you wrote. It exists only when the agent was started with --reconcile-file, and it answers a different question: not whether the kernel is doing what the config says, but whether the config says what your file says. GET /api/drift reports it:

bash
curl -sk -H "Authorization: Bearer $T" "$R/api/drift"
# {"managed":true,"file":"/config/wheelhouse/agent.yaml","mode":"stage",
#  "drifted":true,"ops":2,"checked_at":"2026-09-02T10:27:07Z"}

mode is off for an agent that is managing no file, which is the default. The matching metrics — wheelhouse_desired_managed, wheelhouse_desired_drift, wheelhouse_desired_pending_ops — are exported only while a file is being managed, so their absence means "not managing a file" rather than "no drift".

How live is live#

Different numbers on the same screen have different ages, because every read costs the router work — see What the agent asks the router.

ReadingFreshness
The live stream, shown by the router chip in the top barA fresh, uncached read of show interfaces every 2 seconds over a WebSocket
The throughput samples behind the Overview's current ratesSampled every 5 seconds, 240 samples kept in memory — twenty minutes, not persisted
Other operational readsCached 3 seconds, served stale while a refresh runs
Per-interface detailCached 20 seconds
Configuration readsCached until a commit invalidates them

The stream bypasses the cache, because a stream labelled live cannot replay a three-second cache. An unreadable router arrives as an error frame naming the failure rather than as silence.

What is not compared#

  • The firewall's rules are not compared with the kernel's ruleset. Hit counters are joined from firewall statistics, which tells you a rule is being matched. A per-object nftables ruleset view is not built.
  • IPv6 is observable on its own page. Network → IPv6 shows v6 addresses, neighbours, routes and ip6 counters, and Static routes shows the v6 forwarding table beside the v4 one. The neighbour tables still read the IPv4 table, and the uplink model has no v6 concept.
  • The router's uplink verdict is the router's, and the agent's numbers are its own. The Health badge reports whether the failover daemon's route is in the forwarding table and what the load balancer says. Beside it the agent probes each uplink itself every five seconds and shows per-target round-trip time, jitter and loss — evidence, not the router's verdict, and the page says per uplink whether the router would act on it.
  • Conntrack byte counters need nf_conntrack_acct enabled on the router; without it they are absent rather than zero.
  • Companion services report configuration state. The Companions page reads whether the router's own service dns and service dhcp-server are configured. The agent supervises no external process. Its third panel is UPnP, which is the agent's own service rather than the router's: it reports the agent's UPnP settings and whether its listeners are bound.

See also#

Updated 2026-09-18 concepts drift monitoring