Porthole is a small round window onto how current an app's data is: one line at a time, opened for detail. This page describes the contract it reads: the snapshot a service produces, the heartbeat beside it, and portholeView, the pure function that decides what to show. The window, a CLI status command and an agent read the same snapshot, so they agree.
The snapshot
The service produces one snapshot, plain JSON, and sends it again only when it changes:
const snapshot: PortholeSnapshot = {
version: 1,
items: [{ id: "sync", priority: "working", label: "Catching up… 40%", progress: 0.4, since: 1793731200000 }],
areas: {
chats: { lastVerifiedAt: 1793731195000, troubleSince: null },
calendar: { lastVerifiedAt: null, troubleSince: null, label: "Calendar", freshWithinMs: 5 * 60_000 + 30_000, note: "every 5 min" },
},
};itemsare what the service wants said. An item'spriorityisaction(it waits for the person),error,workingorrest(news that needs nothing, such as "Version 2.1 is installed"). The app's source fillslabelanddetailwith words.areashold verified facts only:lastVerifiedAt, when the area's data was last verified current (null until it has been once), andtroubleSince, when its current read began to hang or fail (null when it has not).- An area the service reads on an interval rather than hears pushed sets its own
freshWithinMs: the interval plus a margin measured from how long a read takes. Live areas keep the view's minute.own Itsnotesays in a few words how the service keeps it current ("every 5 min", "live"), and the area's line in the open spot ends with it, after the area's age once it is over a minute old, as in "Calendar: verified 3 min ago, every 5 min".note - A snapshot of another
versionreads as "Status unknown".version
The heartbeat
The heartbeat is not in the snapshot. The source exposes it apart, as lastBeatAt(), and the service beats on its event stream's keepalive:
const source: PortholeSource = {
getSnapshot: () => latest,
subscribe: (listener) => stream.on("snapshot", listener),
lastBeatAt: () => lastKeepalive,
};What shows
portholeView(snapshot, lastBeatAt, now, options) returns the state, the one line, the item shown and the open spot's lines:
- One item at a time: an action over an error, an error over working, working over rest; on a tie, the newest
sincewins.prioritytie - At rest, "Up to date" only while every area was verified within its
freshWithinMs(the view's, a minute by default, or the area's own); past it, the real age of the area verified longest ago, such as "Updated 4 min ago".defaultsstale - An area whose trouble has lasted
troubleAfterMs(five seconds by default) stops counting as fresh at once, whatever itslastVerifiedAt.trouble - "Status unknown" while any area has never been verified, and once the last beat is older than
heartbeatTimeoutMs(five seconds by default) or stamped too far ahead of this clock to check.defaults A stopped heartbeat outranks every item, an action too, since the action's button would reach a service that cannot answer; the item waits in the open spot's lines.unknownstoppedskew - A rest item shows over "Up to date" only; past fresh, or unknown, it waits in the open spot's lines.rest Shown with an action, it reads as
action, since the person has something to press.rest-action
state is what data-porthole-state carries for a fault-injection check: rest-fresh, rest-stale, unknown, action, error or working. status is the agent read's name for it, with needs-person for an action.
Looking again without a ticking clock
nextChangeAt is the earliest moment the view could change with no new snapshot and no new beat: an area's fresh time running out, an age shown reaching its next minute, the heartbeat timing out, or trouble reaching troubleAfterMs.next Beats do not notify the view, so while the heartbeat is stopped it also looks for its return heartbeatTimeoutMs on.return Schedule one timeout for it, read lastBeatAt() again when it fires, and render only when the view changed. The age words never count seconds, so they change at most once a minute.age
The agent read
portholeStatus(view) is the versioned JSON an agent reads and a CLI status command prints: the state, the headline, whether the data is fresh and the heartbeat alive (with its age), each area's lastVerifiedAt, age, the freshWithinMs it was judged by, its note, troubleSince and troubleMs, what is shown and why it wins, and asOf, the clock it was judged at, so an agent never judges liveness on its own clock.statuslivewindows It names the person's action by id and label (action, actionLabel), and only the person presses it.
Claim sources
- version: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > says Status unknown for a snapshot of another version, whatever its items
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > says Status unknown for a snapshot of another version, whatever its items) - priority: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > shows an action over an error, an error over working and working over rest
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > shows an action over an error, an error over working and working over rest) - tie: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > on a tie in priority, shows the item with the newest since, and the lower id when since ties too
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > on a tie in priority, shows the item with the newest since, and the lower id when since ties too) - defaults: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > uses a minute for fresh, five seconds for the heartbeat and for trouble, and a second for skew by default
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > uses a minute for fresh, five seconds for the heartbeat and for trouble, and a second for skew by default) - stale: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > at rest, says the real age of the area verified longest ago once any area is past fresh
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > at rest, says the real age of the area verified longest ago once any area is past fresh) - trouble: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > a trouble-free area is fresh at 4.9 s, in trouble for 5 s it is not, and it is fresh again once the trouble clears
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > a trouble-free area is fresh at 4.9 s, in trouble for 5 s it is not, and it is fresh again once the trouble clears) - unknown: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > says Status unknown at rest while any area has never been verified
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > says Status unknown at rest while any area has never been verified) - stopped: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > a stopped heartbeat outranks every item, an action too, which waits in the open lines
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > a stopped heartbeat outranks every item, an action too, which waits in the open lines) - skew: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > says Status unknown when the heartbeat is stamped too far ahead of this clock, or missing, and accepts skew within the tolerance
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > says Status unknown when the heartbeat is stamped too far ahead of this clock, or missing, and accepts skew within the tolerance) - rest: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > a rest item shows over Up to date only, and waits in the open lines while an area is past fresh or the status is unknown
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > a rest item shows over Up to date only, and waits in the open lines while an area is past fresh or the status is unknown) - rest-action: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > a rest item that offers an action reads as an action, for the person, and as needs-person to an agent
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > a rest item that offers an action reads as an action, for the person, and as needs-person to an agent) - next: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > nextChangeAt > at rest and fresh, is the first of the heartbeat timing out and an area's fresh time running out
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > nextChangeAt > at rest and fresh, is the first of the heartbeat timing out and an area's fresh time running out) - return: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > nextChangeAt > with the heartbeat stopped, looks for its return a heartbeat timeout on, and sees it come back from a stale, a missing or a NaN beat
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > nextChangeAt > with the heartbeat stopped, looks for its return a heartbeat timeout on, and sees it come back from a stale, a missing or a NaN beat) - age: test:registry/ouro/lib/ouro/porthole-view.test.ts::formatPortholeAge > rounds down to minutes, hours and days, and never counts seconds
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::formatPortholeAge > rounds down to minutes, hours and days, and never counts seconds) - status: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeStatus > reads as versioned JSON with needs-person for an action, and survives a round trip
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeStatus > reads as versioned JSON with needs-person for an action, and survives a round trip) - live: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeStatus > says whether the data is fresh and the heartbeat alive, whatever is shown, as of the clock it was judged at
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeStatus > says whether the data is fresh and the heartbeat alive, whatever is shown, as of the clock it was judged at) - own: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > an area's own freshWithinMs > replaces the view's for that area alone, up to and including the window itself
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > an area's own freshWithinMs > replaces the view's for that area alone, up to and including the window itself) - note: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > an area's own freshWithinMs > gives a fresh area's age past a minute, and its note, in its line
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeView > an area's own freshWithinMs > gives a fresh area's age past a minute, and its note, in its line) - windows: test:registry/ouro/lib/ouro/porthole-view.test.ts::portholeStatus > gives each area the window it was judged fresh by and its note
verify:testExists(registry/ouro/lib/ouro/porthole-view.test.ts::portholeStatus > gives each area the window it was judged fresh by and its note)
