Skip to content

Complete Dashboard freshness semantics for failed and superseded refresh waves #441

Description

@BorisTyshkevich

Summary

Follow-up from the review of #440. The compact Dashboard freshness control is acceptable for the toolbar-density work in #437, but its remaining outcome semantics should be completed separately rather than blocking that PR.

The current implementation introduces:

  • lastSuccessWallMs, a stable wall-clock timestamp for the last successful full refresh;
  • lastRefreshOutcome, currently success | failure | null;
  • a visible and accessible refresh-failure state.

Three edge cases remain:

  1. a failed refresh replaces the visible timestamp instead of preserving it;
  2. filter-source/helper failures can still be classified as a successful refresh;
  3. a superseded partial refresh can be recorded as successful even though it did not complete its full intended wave.

Current behavior

Failure hides the visible last-good time

When lastRefreshOutcome === 'failure', the renderer currently sets:

updated.textContent = 'Refresh failed';

The prior successful timestamp remains only in the refresh button's tooltip and accessible label.

That conflicts with the compact control's intended visual contract:

16:31  ↻

A failure should preserve the visible last-known-good time when one exists and layer an error state onto the same compact control.

Refresh success is derived only from tile errors

The current outcome classifier checks only the tiles passed to it:

const failed = ranTiles.some((runtime) => runtime.state.status === 'error');

A full Dashboard refresh also runs and merges Filter sources. Those paths can terminate with:

  • source-query/preparation failure;
  • source-error;
  • helper-error;
  • missing-helper;
  • error diagnostics from the helper merge.

The affected panel query may still complete successfully, so tile status alone can produce a false success while the Dashboard visibly reports a broken Filter source or helper.

Superseded refreshes can claim success

When a full refresh's Filter wave is superseded by a concurrent selective update, the refresh delegates its affected tiles to the newer wave. The current branch still calls:

recordRefreshOutcome(unaffected, waveMs);

If unaffected is empty, the error check is vacuously false and the incomplete full refresh is recorded as successful. With a non-empty unaffected set, it can still advance the global Dashboard success timestamp even though the refresh did not complete the affected portion it intended to run.

A superseded full refresh is incomplete, not successful or failed.

Required behavior

Visible failure treatment

After a failed refresh:

  • preserve lastSuccessWallMs;
  • when a prior successful timestamp exists, keep that timestamp visible in .dash-updated;
  • expose failure through the compact control without replacing the visible time;
  • tint or otherwise mark the time and refresh action as an error using the existing semantic error vocabulary;
  • keep a descriptive tooltip and accessible label, for example:
Refresh failed. Last successfully updated at 16:31
  • when no refresh has ever succeeded, visible Refresh failed text is acceptable because there is no timestamp to preserve;
  • a later successful refresh clears the failure state and advances the visible timestamp.

Do not permanently widen the ordinary compact control merely to show an inline error sentence.

Whole-wave outcome classification

A full refresh() may record success only after every part owned by that refresh reaches a clean terminal state.

At minimum, classify the wave as failed when any of the following belongs to that completed refresh:

  • a tile run by the wave ends in error;
  • a Filter source run by the wave ends in error;
  • a Filter consumer ends in source-error, helper-error, or missing-helper;
  • the terminal helper merge produces an error diagnostic.

waiting, unfilled, and ordinary warning/info diagnostics should retain their existing semantics unless the implementation can show they represent a broken refresh rather than a valid incomplete-input state.

Do not infer the outcome solely from the final global tile array: concurrent selective waves may own some of those statuses by the time the older refresh settles. Track the records/generations actually owned by the full refresh.

Superseded outcome

A full refresh whose source wave is superseded must not update either:

  • lastSuccessWallMs;
  • the most recent completed success/failure outcome shown by the freshness control.

Represent this explicitly if useful, for example:

type RefreshOutcome = 'success' | 'failure' | 'superseded' | null;

Alternatively, return an internal terminal result and skip publishing a new user-visible outcome for superseded. The exact representation is implementation-defined; the required behavior is that incomplete work never claims a new successful refresh.

The newer selective wave may update tile/filter data normally, but it must not retroactively make the superseded full refresh a clean full-Dashboard refresh unless the application deliberately tracks and awaits all delegated work under one combined operation.

Suggested design

Introduce a per-full-refresh outcome accumulator rather than deriving success from a tile array at the end.

An illustrative internal shape:

interface FullRefreshResult {
  status: 'success' | 'failure' | 'superseded';
  tileFailures: string[];
  sourceFailures: string[];
  helperFailures: string[];
}

Suggested flow:

  1. capture the full refresh generation and waveMs;
  2. run the unaffected tile batch and Filter-source wave;
  3. if the source wave is superseded, return superseded without updating freshness state;
  4. run the affected tile batch;
  5. classify only work owned by this generation;
  6. on clean success, set lastSuccessWallMs = waveMs and outcome success;
  7. on failure, preserve lastSuccessWallMs and set outcome failure;
  8. publish the terminal state once with the correct outcome.

Avoid coupling the freshness model to updatedAt; that field uses the monotonic performance clock and is not suitable for display as wall time.

Unit tests: viewer session

Extend tests/unit/dashboard-viewer-session.test.ts.

Required coverage:

  1. Filter-source transport failure

    • the panel query succeeds;
    • the Filter source query fails;
    • the full refresh outcome is failure;
    • the prior lastSuccessWallMs is preserved.
  2. Helper failure

    • source transport succeeds but produces helper-error or missing-helper;
    • the full refresh is not recorded as a clean success.
  3. Superseded with no unaffected tiles

    • every tile is Filter-affected;
    • a selective wave supersedes the full source wave;
    • the full refresh does not advance the timestamp or outcome.
  4. Superseded with unaffected tiles

    • some unaffected work completes;
    • the affected side is delegated to a newer wave;
    • completing only the unaffected subset does not count as a successful full refresh.
  5. Clean recovery

    • a later complete clean refresh advances the timestamp and clears failure.
  6. Destroyed/stale generation

    • destroyed or stale work never updates freshness state.

Unit tests: Dashboard renderer

Extend tests/unit/dashboard.test.ts.

Required coverage:

  1. after a successful refresh, record the visible time;
  2. fail a later refresh;
  3. assert the same time remains visible;
  4. assert .dash-freshness exposes the error state;
  5. assert the refresh button label/title says:
Refresh failed. Last successfully updated at <time>
  1. cover a first-ever failure with no prior successful timestamp;
  2. cover successful recovery;
  3. preserve the existing stability assertions for Search, layout, and document-only publications.

Browser coverage

Add or extend focused Playwright coverage for the compact freshness control:

  • successful refresh shows a compact visible time;
  • failed refresh retains that visible time and exposes the error treatment;
  • the tooltip/accessible name identifies both failure and the last successful time;
  • recovery clears the failure styling;
  • View/Edit remains immediately after the compact control at desktop and mobile widths.

Regression boundaries

The change must not alter:

  • Dashboard query/filter execution ordering;
  • stale-generation and cancellation guards;
  • selective filter-wave ownership;
  • tile/filter error rendering;
  • updatedAt's existing internal completion semantics;
  • waveWallNowMs time-range resolution;
  • Search/layout/document-only timestamp stability;
  • refresh spinner and aria-busy behavior;
  • toolbar width, ordering, and mobile horizontal scrolling;
  • View/Edit placement;
  • ordinary tile-level refresh behavior.

Acceptance criteria

  • A failed refresh with a prior successful run keeps the prior time visible.
  • Failure remains clear visually and through the refresh button's tooltip and accessible name.
  • A first-ever failed refresh has a clear no-timestamp failure state.
  • Tile, Filter-source, and helper failures are included in full-refresh outcome classification.
  • A superseded full refresh never advances the last-success timestamp or claims a new success.
  • Outcome decisions use only work owned by the relevant refresh generation.
  • A later clean full refresh clears failure and advances the wall-clock timestamp.
  • Unit and browser regression coverage is included.

Related work

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions