opsinjs
ComponentsPatternsTask patterns

Offline and stale data

What a health screen shows when it cannot vouch for its numbers. It degrades assertions rather than hiding them, and never lets a cached value pass as current.

When to use

Use this pattern when the product is showing a value it can no longer stand behind: the network is gone, a device has not synced, a result was fetched an hour ago, or a background refresh silently failed.

The core rule is that staleness degrades assertions, not values. The number stays on screen. Hiding it helps nobody, and a reader who wanted to see their last reading is entitled to it. What changes is everything the product says about the number: the status, the trend direction, the comparison to a range, the alert. Those are claims about now, and the product no longer knows about now.

Signals:

  • The screen shows a status pill computed from data that may have moved.
  • A device is the source and the pairing may have dropped.
  • A reader could reasonably act on this screen while it is wrong.

When not to use

  • There is no value at all. That is Empty and first-use. Stale means you have something and cannot vouch for it.
  • The value is fine and old by design. A result from a blood test in March is not stale; it is dated. Show its date and stop. Staleness is about the gap between what you are asserting and what you know.
  • You want the doctrine. How to render "we do not know" honestly, and the vocabulary for it, is Uncertainty and staleness.
  • You want provenance. Who measured this and with what is Data provenance and device accuracy.

How it works

Diagram source (mermaid)
flowchart TD
A["Screen renders from cache"] --> B{"How old is the value?"}
B -->|"within the freshness window"| C["Full assertions: status, range, trend"]
B -->|"beyond the window"| D["Demote: value and timestamp stay, status is withdrawn"]
D --> E{"Is a refresh possible?"}
E -->|"yes"| F["Refresh quietly; announce only on change or failure"]
E -->|"no"| G["State that the app is offline and what that means here"]
F --> H{"Did it succeed?"}
H -->|"yes"| I["Promote back to full assertions"]
H -->|"no"| G
G --> J["Queued writes stay visible and marked unsynced"]

NOT IMPLEMENTED. This component does not exist in any released version of opsinjs. There is no package to install, no module to import and no props interface to generate code against. Everything on this page is a specification of intended behaviour and may change without notice. Do not write code against it.

This component is not built yet

There is nothing to render because there is nothing to install. What you can read on this page is the specification the implementation will have to satisfy.

PlannedRoadmapWhat “planned” means

  • Every value carries a freshness window, and the window belongs to the measure. A weight from this morning is fresh; a continuous glucose reading from twenty minutes ago may not be. The component does not decide this; the team that owns the thresholds does.
  • Beyond the window, the status axis is withdrawn. No StatusPill, no colour-coded verdict, no trend arrow. The value and its RelativeTime remain. This is the whole pattern in one rule.
  • Never resolve staleness by removing the timestamp. "Last synced 4 hours ago" is uncomfortable; a screen that looks live and is not is worse.
  • Alerts computed from stale data are suspended, not fired. An escalation based on a value you cannot vouch for is the worst combination of confident and wrong. See Alert escalation.
  • Writes queue and stay visible. An entry made offline appears immediately, marked as not yet synced, with the time it was actually made. It is never the time it eventually uploaded.
  • Conflicts are shown, not merged. Two devices writing the same slot produce two entries the reader can see and resolve. Silent last-write-wins destroys data the reader believes is stored.
  • Recovery is quiet on success and loud on failure. Coming back online should not throw a banner; failing to reach the server after a queued write should.

States

StateValueStatus axisTimestamp
FreshShownFullDe-emphasised
AgeingShownFull, with the age promotedVisible
StaleShownWithdrawnProminent
Offline, cachedShownWithdrawnProminent, plus an offline notice
Unsynced writeShownNone yetTime of entry, marked unsynced
ConflictBoth shownWithdrawn until resolvedBoth times shown
Unknown ageShownWithdrawn"We don't know when this was measured"

That last row is not a hypothetical. A value restored from a backup or imported from a third party frequently has no trustworthy timestamp, and the honest rendering is to say so rather than to stamp it with the import time.

Content

Stale copy has to be specific about what is uncertain, because a vague warning makes the reader distrust the whole screen rather than the one number.

Do

"Last synced 4 hours ago. Your monitor may have taken readings since then. We can't show a current status until it reconnects."

Don’t

"Some data may be out of date." Which data, how out of date, and what should the reader do differently?

Do

"Saved on your phone. It will sync when you're back online." The entry is safe, the reader knows the state.

Don’t

A spinner over the save button until the network returns, with the reader unsure whether their reading was recorded at all.

Relative-time wording follows Numbers, dates and time. That page covers "4 hours ago" versus "at 06:12" and when each is appropriate.

Accessibility

  • Timestamps are announced in full. A RelativeTime renders "4 hours ago" visually and exposes the absolute time to assistive technology; "4h" is not a word.
  • Entering the stale state is a status message (WCAG 2.2 SC 4.1.3), polite, once per transition rather than once per tile. A dashboard with nine tiles going stale simultaneously must announce once.
  • The offline notice does not steal focus. It is announced, not modal, and the reader is not interrupted mid-entry.
  • Do not signal staleness by reducing contrast. Greying a value out is the most common approach and it fails the contrast floor for exactly the reader who needed it most. Staleness is carried by words and position; see The contrast floor.
  • Retry is a control, not a gesture. Pull-to-refresh may exist; a focusable, labelled retry must also exist. See Target size and motor.

Research

Updates to this page

Last read through against the system on 2026-09-20. Due for review every 6 months; expiry is reported by pnpm run check:freshness.

On this page