T04P

A live event-operations dashboard that modeled freshness and continuity instead of equating an open socket with truth.

Personal project

Project brief

Period
2013
Status
Archived
Focus
Truthful real-time state
Constraint
An open connection could not be treated as current knowledge.
Result
Freshness, continuity, and source state became explicit.

T04P was a 2013 personal project built to explore real-time interfaces. C62Y had taught me how a document could respond to space and capability. T04P asked whether an interface could respond to time without becoming misleading.

A harder dimension than width

I invented a small event-operations scenario with three feeds: entry counts, equipment state, and short operator notes. A simulator produced ordinary traffic, delayed updates, duplicated messages, out-of-order delivery, and complete source outages. The browser dashboard was meant to give a person one place to understand the event’s current condition.

Node.js and Socket.IO made the first demonstration gratifyingly easy. Values moved without a refresh. A connection light pulsed. The page felt alive. I showed it to a few friends, and everyone understood the appeal immediately.

Then I paused one upstream simulator while leaving the WebSocket connection open. The dashboard stayed green and continued displaying the last plausible values. Nothing looked broken. The interface was active, connected, and wrong.

That failure became the project. T04P stopped being a demonstration of pushed updates and became an attempt to model freshness, continuity, and partial failure honestly.

What “live” failed to describe

The first state model had two values: connected and disconnected. It collapsed several independent facts:

  • Whether the browser could reach the dashboard service.
  • Whether the service could reach a particular upstream source.
  • When that source had last observed the world.
  • Whether the browser had received every revision since its current snapshot.
  • Whether the latest value was still useful when old.

A WebSocket heartbeat answered only the first question. It could not make an entry counter current or prove that a generator status had been observed recently.

I replaced the single connection flag with a small model that stayed visible in the code and the interface:

transport: connecting | open | closed
feed: loading | current | delayed | stale | unavailable
revision: integer
observedAt: source timestamp
receivedAt: dashboard timestamp
continuity: complete | recovering | unknown

The values were intentionally plain. I wanted every visual state to correspond to a named condition rather than a general mood such as healthy or unhealthy.

Each feed declared an expected reporting interval. An entry counter that normally changed several times a minute crossed its freshness thresholds sooner than an equipment inspection reported every five minutes. A threshold could not be shared merely because both values appeared on one screen.

Building the source simulator first

The simulator became the most important development tool in the project. If it only emitted a perfect sequence, I could only build a perfect-sequence interface.

Every scenario was deterministic from a seed. It could:

  • Delay one source while the others remained current.
  • Deliver a repeated event with the same stable identifier.
  • Skip a revision and later send a newer one.
  • Reorder two observations in transport.
  • Restart an adapter with an empty in-memory cursor.
  • Change the source clock by a known offset.
  • Disconnect the browser during a burst and reconnect after history expired.

The simulator exposed controls for each fault and displayed what it had emitted independently of what the dashboard showed. This gave me a modest source of truth for debugging. More importantly, it made failure states easy enough to inspect that I actually designed them.

An early scenario replayed the same message with a new transport timestamp. The dashboard treated it as fresh because I had used receipt time as observation time. Preserving both timestamps fixed the immediate defect and clarified a lasting principle: recent delivery does not imply recent knowledge.

Normalizing unlike feeds

The three sources did not naturally speak one language. Counts were frequent numeric observations. Equipment state was a slowly changing category. Notes were human-authored events. Forcing them into identical payloads would have hidden useful differences.

I created a small common envelope around source-specific data:

sourceId
eventId
revision
observedAt
receivedAt
kind
payload

The envelope supported ordering, deduplication, age calculation, and source identity. The payload kept its domain shape. Adapters were responsible for converting an upstream message into this form and for declaring what their health signal could prove.

That last clause mattered. A successful poll could prove that the adapter reached an API. It might not prove that a physical sensor had changed or that an upstream batch was complete. The adapter exposed the narrowest defensible claim instead of returning a universal ok.

The service retained a compact current snapshot and a bounded ring of recent revisions. It was not a log warehouse. T04P needed enough history to reconnect safely and explain a recent transition, not indefinite event storage.

Reconnection was a data operation

Socket.IO could re-establish transport, but transport recovery did not restore application continuity automatically. If the browser missed revisions 42 through 47 and then received 48, it had a newer value and an incomplete sequence.

On reconnect, the browser sent the last fully applied revision for each feed. The service chose one of two responses:

  1. Return a contiguous delta when the bounded history still contained every missing revision.
  2. Return a complete snapshot when continuity could not be proved.

While this handshake ran, the feed entered recovering. It did not display one new message as proof of current state. Out-of-order and repeated revisions were ignored after being recorded in the diagnostic view.

The snapshot itself carried the latest source observation time and the service revision at which it had been assembled. Applying it was one state transition, avoiding a moment where half the page described the restored state and half described the previous one.

Reconnect attempts used jitter. When I restarted the local service with several browser windows open, immediate retry loops created a small thundering herd. The scale was trivial, but the shape of the failure was already visible. Randomized delay made the recovery smoother and taught me not to reserve distributed-systems thinking for large systems.

The dashboard was supposed to support a quick reading, so I resisted filling every region with timestamps and warnings. Current data stayed visually quiet. Context appeared as confidence fell.

A delayed feed gained a subtle age label. A stale feed added an explicit state word and border treatment. An unavailable feed either retained its last known value with a prominent observation time or replaced the value with an explanation, depending on whether stale information remained useful in that domain.

Text and shape accompanied color. The design could not depend on a green or red distinction. Movement was removed from the trust language entirely; animation indicated a transition in progress, never evidence that the data was alive.

I separated transport status from feed status in both the overview and diagnostic view. A person could see “dashboard connection open” beside “entry feed stale.” This felt more complicated than one green indicator, but it matched reality and reduced the more dangerous complexity of false certainty.

Operator notes joined the timeline as attributed observations. A person could record that a door appeared blocked or a sensor had been checked. The note did not overwrite automated data. It sat beside it with an author, time, and source label.

Age looked like a subtraction problem and became a lesson in clocks.

The source supplied an observation time. The adapter recorded receipt time. The browser had its own clock. If I computed age only in the browser, a badly set device clock could make current data appear to come from the future. If I rewrote source time during ingestion, I erased evidence about the source.

The service estimated known clock offset where a source offered a trustworthy synchronization signal. The event retained the original timestamp, received timestamp, and any applied display correction. The interface used the corrected value for a readable age while the diagnostic view exposed the underlying times.

I did not attempt perfect global ordering. Events from different sources could be displayed by a normalized presentation time with a marker when order remained uncertain. Within one feed, the monotonic revision was more dependable than wall-clock time.

This distinction later became central to W93H. A visually neat timeline is tempting, but a system should not silently manufacture chronology from clocks it cannot trust.

Running continuously changed the frontend

Most of my earlier pages were loaded, read, and left. T04P was meant to remain open. A browser tab that runs for hours reveals different defects.

The first version appended every note and state transition to the DOM. Memory grew, rendering slowed, and the timeline became unusable. I bounded visible history and kept the service snapshot compact. The interface offered a small recent window rather than pretending to be a permanent archive.

Event listeners leaked during reconnect because initialization attached a second set without removing the first. I added explicit connection lifecycle tests and a diagnostic counter for active subscriptions. A long-running soak scenario repeatedly interrupted transport while recording heap use and event application counts.

The clock that updated age labels once per second triggered far more work than the display required. Most labels only changed meaningfully at a threshold or minute boundary. A shared scheduler calculated the next relevant update and avoided independent timers for every row.

Background tabs were throttled by browsers. When the page became visible again, it recalculated freshness from timestamps and requested continuity instead of assuming its timers had run. The tab’s experience of time was not the system’s evidence of time.

I initially logged every incoming event and render decision to the console. The volume made failures harder to see. T04P needed diagnostics shaped around its model.

A small developer view showed:

  • Transport connection and last heartbeat.
  • Each adapter’s claimed health and latest source observation.
  • Current snapshot revision and browser-applied revision.
  • Duplicate, out-of-order, and skipped event counts.
  • Last recovery method: delta or snapshot.
  • Current freshness threshold and next transition.

The server logged state transitions rather than every value. “Feed moved from current to stale because observation age crossed 90 seconds” was more useful than another copy of the unchanged count.

This was the first time I thought of observability as an interface for a system’s own operator. The diagnostic view did not need the visual polish of the main board, but it needed the same honesty about what was observed and inferred.

The operator-note feature began as a tiny text box under the dashboard. The first implementation behaved like chat: submit a string, append it at the bottom, and let the newest message become the most visible. That model encouraged commentary without context. “Looks normal again” did not identify which feed had been checked, what normal meant, or whether the note corrected an earlier observation.

I narrowed notes into attributed observations. A note could reference a feed or the whole event, carried an author label from the small test roster, and preserved both the author’s observation time and the service receipt time. A later note could supersede an earlier one without deleting it. Short category labels distinguished a physical check, a communication update, and an operating decision.

The form prompted for observation rather than conclusion. Instead of asking “What happened?”, it asked what the person could currently see and which source it concerned. This did not guarantee careful notes, but it reduced the interface’s invitation to write unsupported diagnoses.

Notes were delivered through the same revision stream as automated events so every browser converged on one order. They did not share the same visual encoding. A source update represented machine-reported evidence; a note represented a person’s attributed account. The board treated both as useful and neither as interchangeable.

I added an offline draft only after losing a carefully written note during a reconnect. The draft remained local until the connection recovered, at which point the interface asked the author to confirm that the observation was still relevant. I did not silently submit it later because the temporal context of an operational note matters. This small feature foreshadowed the harder offline-intent questions in X8B6.

A review ritual for failures

Every fault scenario ended with a short review rather than only a pass or fail. I captured the sequence emitted by the simulator, the service transitions, the state shown to the reader, and any moment where the interface’s claim exceeded its evidence.

The review used a few recurring questions:

  • Did the first visible change appear before or after confidence actually fell?
  • Could a reader distinguish stale data from a broken browser connection?
  • Did recovery explain whether it used a delta or replaced the snapshot?
  • Was the last known value still labeled with its real observation time?
  • Could duplicated or reordered delivery change the final projection?
  • Did a human note remain clearly attributed after it joined the stream?

I kept a collection of scenario transcripts in the repository. A change to the state reducer replayed them and compared the resulting state transitions. The comparison was not a screenshot test. It asserted that, for example, a revision gap moved continuity to unknown, and that only a successful snapshot could return it to complete.

Visual review then checked whether those named states remained understandable. One redesign made delayed and stale technically distinct but visually almost identical. Test readers could not explain the difference, so I simplified the display language while retaining the detailed state internally. A model may need precision that an overview should summarize.

The ritual prevented a common failure of personal projects: polishing whatever the happy-path demo happened to exercise. Each session began with an inconvenient condition, and the interface had to earn its calm state again.

The exciting version of a real-time system wanted to accumulate features: permanent history, arbitrary queries, alert rules, user presence, replay, and integrations. I deliberately kept T04P centered on a current operational view.

The service stored one normalized snapshot, a bounded reconnect window, and a short note history. When an event aged out, a disconnected browser received a snapshot rather than a partial reconstruction. This traded fine-grained replay for a system whose memory and recovery behavior I could explain.

Backpressure was equally modest. Each browser received state changes through one ordered stream. If its outgoing queue crossed a limit, the service stopped appending deltas, marked that session for snapshot recovery, and sent a new snapshot after the connection became writable. Allowing an unbounded queue would have preserved every event in theory while making the browser increasingly historical in practice.

The simulator included a slow-consumer mode to verify this path. The first version dutifully delivered the entire backlog and showed the “current” count racing through several minutes of old values. Snapshot recovery made the board useful sooner and revealed that completeness at the transport layer was not always the product goal.

I added basic origin checks and constrained note payloads even though the project ran in a small personal environment. A live connection and text input create ordinary security responsibilities. The code never evaluated feed names or note content as markup, generated session identifiers independently of user input, and limited message size before broadcast.

These boundaries made the project less impressive as a platform and more successful as a learning system. I could state what it retained, when it discarded history, how it recovered, and which claims its status indicators were allowed to make.

What the first versions got wrong

The pulsing green connection marker was the clearest failure. It communicated activity as truth and survived exactly the failure it should have revealed. Removing it was not a cosmetic adjustment; it changed the product’s claim.

Freshness initially lived behind a diagnostic control because I considered it technical detail. Test readers could not distinguish a calm event from a frozen feed. Age and state moved beside the value.

I also used one global stale timeout. It made slow equipment updates look broken and allowed a fast entry counter to remain falsely current. Feed-specific expectations replaced the universal threshold.

The reconnect prototype accepted the newest event and cleared its warning. A skipped-revision scenario proved that newness and continuity were separate. Recovery became an explicit snapshot-or-delta protocol.

Finally, the timeline placed human notes and source events in identical rows. Readers inferred that both had the same evidentiary status. The redesign gave observations, actions, and notes distinct labels without making one visually unimportant.

What became more difficult than C62Y

C62Y negotiated stable content against changing presentation conditions. T04P added data whose meaning decayed while it sat on the screen. Correct markup and responsive layout were necessary and insufficient.

The project forced me to design four connected layers:

  • A simulator capable of producing inconvenient reality.
  • A service that preserved identity, order, and source claims.
  • A recovery protocol that distinguished transport from continuity.
  • An interface that revealed uncertainty without becoming alarmist.

Each layer constrained the others. A freshness label was only credible if the source timestamp survived ingestion. A reconnect message was only useful if revisions had stable identity. A diagnostic view was only clear if the state model used named transitions.

The result was not a large system, but it was my first project where the interface’s main responsibility was epistemic: show what the system knows, how recently it knew it, and where the evidence is incomplete.

T04P’s current snapshot lived primarily on the server. If the browser disconnected, it could wait and recover. The next problem removed that comfort. X8B6 would let a person create important new intent while offline, preserve it for hours, and reconcile it later without duplication or silent loss.

The revision and snapshot ideas carried forward, but they were no longer enough. T04P recovered observations. X8B6 would have to recover actions.

The dashboard also planted a theme that returned in every later project: a simple status word is often a lossy compression of several independent facts. “Online,” “synced,” “resolved,” “grounded,” and “complete” all deserve to be unpacked before the interface promises them.

What the dashboard could not know

T04P used simulated sources and a deliberately small event model. It could not prove the behavior of physical sensors or real venue networks. Its bounded history supported short recovery, not audit-grade retention. Some sources could only report successful polling, not underlying physical change.

The project did not infer causation and did not replace source-specific tools. It condensed a few operational signals into a truthful current view. When evidence was missing, the responsible result was a visible unknown state, not a more confident animation.

Technology: Node.js, Socket.IO, server-rendered HTML, a small client-side state reducer, deterministic source simulators, revisioned snapshots, bounded event history, and structured operational diagnostics.