Skip to content

feat(freshness): surface seed health in risk panel#3493

Open
lspassos1 wants to merge 5 commits into
koala73:mainfrom
lspassos1:feat/data-freshness-surface
Open

feat(freshness): surface seed health in risk panel#3493
lspassos1 wants to merge 5 commits into
koala73:mainfrom
lspassos1:feat/data-freshness-surface

Conversation

@lspassos1
Copy link
Copy Markdown
Collaborator

Summary

Surfaces seed-health freshness in the Strategic Risk panel by ingesting /api/health cadence metadata into the existing dataFreshness tracker. Analysts get a compact data freshness signal without blocking the main risk refresh path.

cc @koala73

Refs #3296

Type of change

  • New feature
  • Refactor / code cleanup

Affected areas

  • API endpoints (/api/*)
  • Other: Strategic Risk panel freshness UI and health ingestion

Root cause

The frontend freshness tracker was mostly session-local. The backend already knows seed age, cadence, and health state, but that signal was not visible in the Strategic Risk panel, so stale or missing seeded data could be hard to distinguish from genuinely quiet conditions.

Changes

  • Add refreshDataFreshnessFromHealth() to fetch /api/health, map health checks to frontend data sources, and record cadence-aware seed health.
  • Extend dataFreshness with seed-health metadata, per-source maxStaleMin, and error/no-data handling for health statuses.
  • Surface a compact Data Freshness list in StrategicRiskPanel with source state and last-update timing.
  • Run health freshness polling as a non-blocking background task so risk rendering is not serialized behind /api/health.
  • Localize the active/total source badge detail.
  • Add drift and severity tests for health-check mapping, shared-source worst-status selection, and Redis outage status handling.

Validation

Risk

Low to moderate. The health fetch is additive and non-blocking; if it fails, existing session-local freshness remains in use. The mapping guard test should catch future drift between frontend source IDs and /api/health check names.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 2026

@lspassos1 is attempting to deploy a commit to the World Monitor Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 28, 2026

Greptile Summary

This PR surfaces backend seed-health metadata from /api/health in the Strategic Risk panel by adding a new health-freshness service, extending dataFreshness.recordSeedHealth() with cadence-aware thresholds, and rendering a compact per-source freshness list. The health fetch runs as a non-blocking background task so the main risk refresh path is unaffected, and a drift-detection test guards against the frontend source map falling out of sync with the backend check registry.

Confidence Score: 4/5

Safe to merge; only P2 findings present, both isolated to StrategicRiskPanel.refreshHealthFreshness().

All findings are P2. The dead .catch() is harmless dead code, and the optimistic throttle timestamp is a minor resilience gap on a non-critical background path. Core logic in health-freshness.ts and data-freshness.ts is correct.

src/components/StrategicRiskPanel.ts — refreshHealthFreshness throttle stamping and dead catch handler.

Important Files Changed

Filename Overview
src/components/StrategicRiskPanel.ts Adds non-blocking health freshness polling and a compact freshness surface to the risk panel; two minor issues: dead .catch() handler and optimistic throttle-timestamp stamping before the fetch completes.
src/services/health-freshness.ts New service that fetches /api/health, maps check names to frontend source IDs, applies "worst status wins" per shared source, and forwards updates to dataFreshness.recordSeedHealth(). Logic is well-structured with correct deduplication and staleness-ratio tiebreaking.
src/services/data-freshness.ts Extends DataSourceState with maxStaleMin/healthStatus, adds recordSeedHealth() that correctly derives status via calculateStatus (which already handles lastError → 'error' priority), and refactors threshold computation to use per-source cadence.
src/locales/en.json Adds two i18n keys (dataFreshness and sourcesDetail) required by the new freshness surface and badge detail; changes are minimal and correct.
tests/data-freshness-health.test.mts Four well-targeted test cases covering cadence hydration, drift detection against api/health.js, worst-status selection for shared sources, and Redis outage severity ranking; assertions are correct against the implementation logic.

Sequence Diagram

sequenceDiagram
    participant Panel as StrategicRiskPanel
    participant HF as health-freshness.ts
    participant API as /api/health
    participant DF as dataFreshness (singleton)
    participant UI as render()

    Panel->>Panel: refresh()
    Panel-->>HF: void refreshHealthFreshness() [non-blocking]
    Panel->>DF: getSummary()
    Panel->>UI: render() → renderFreshnessSurface()
    UI->>DF: getAllSources()
    DF-->>UI: sources (status recalculated)

    Note over HF,API: runs concurrently
    HF->>API: GET /api/health
    API-->>HF: { checkedAt, checks: { ... } }
    HF->>HF: map check names → DataSourceId[]
    HF->>HF: deduplicate (worst-status wins per source)
    HF->>DF: recordSeedHealth(updates)
    DF->>DF: calculateStatus() with maxStaleMin cadence
    DF-->>Panel: notifyListeners() → triggers next refresh
Loading

Reviews (1): Last reviewed commit: "fix(freshness): decouple health polling ..." | Re-trigger Greptile

Comment thread src/components/StrategicRiskPanel.ts Outdated
Comment on lines +85 to +87

public async refresh(): Promise<boolean> {
void this.refreshHealthFreshness().catch((error) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Dead .catch() handler — never fires

refreshHealthFreshness() has its own try/catch that swallows every exception without rethrowing, so the promise it returns can never reject. The .catch() registered here is unreachable dead code and will mislead future readers into thinking errors can surface from this call.

Suggested change
public async refresh(): Promise<boolean> {
void this.refreshHealthFreshness().catch((error) => {
void this.refreshHealthFreshness();

Comment thread src/components/StrategicRiskPanel.ts Outdated
Comment on lines +171 to +172
if (now - this.lastHealthFreshnessRefreshAt < 60_000) return;
this.lastHealthFreshnessRefreshAt = now;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Timestamp stamped before fetch — failed requests lock out retries for 60 s

this.lastHealthFreshnessRefreshAt = now is written before await refreshDataFreshnessFromHealth(...). If the request fails (network error, non-200, abort), the timestamp is still recorded and the next 60 seconds of refreshHealthFreshness() calls return early, silently skipping the retry window. Setting the timestamp only after a successful call would allow faster recovery when /api/health is momentarily unavailable.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44d6ff7399

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/services/health-freshness.ts Outdated
Comment on lines +99 to +103
const payload = await resp.json() as HealthResponse;
const checkedAtMs = payload.checkedAt ? Date.parse(payload.checkedAt) : Date.now();
const updatesBySource = new Map<DataSourceId, SeedHealthUpdate>();

for (const [checkName, check] of Object.entries(payload.checks ?? {})) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate top-level REDIS_DOWN health response to sources

refreshDataFreshnessFromHealth() only consumes payload.checks, but /api/health returns a top-level { status: 'REDIS_DOWN' } with no checks when Redis is unavailable. In that path this code applies zero updates and preserves prior freshness state, so the Strategic Risk panel can continue to display previously fresh sources during a full backend outage instead of surfacing an error/degraded condition.

Useful? React with 👍 / 👎.

Comment on lines +166 to +167
source.lastError = this.healthStatusIsError(update.status) ? update.status : null;
source.lastUpdate = this.healthStatusHasNoData(update.status)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Degrade COVERAGE_PARTIAL instead of reporting fresh status

Seed-health ingestion only maps SEED_ERROR/REDIS_* to lastError and EMPTY* to no-data; statuses like COVERAGE_PARTIAL keep lastUpdate and are then scored purely by age. When a check is coverage-partial but recent (for example, low-record chokepoint data), this path classifies it as fresh, so the new freshness UI can show a green source even though /api/health explicitly flagged degraded coverage.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88b32fc05f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const freshThreshold = source.maxStaleMin ? source.maxStaleMin * 60_000 : FRESH_THRESHOLD;
const staleThreshold = source.maxStaleMin ? source.maxStaleMin * 2 * 60_000 : STALE_THRESHOLD;
const veryStaleThreshold = source.maxStaleMin ? source.maxStaleMin * 3 * 60_000 : VERY_STALE_THRESHOLD;
if (age <= freshThreshold) return source.healthStatus === 'COVERAGE_PARTIAL' ? 'stale' : 'fresh';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat STALE_SEED checks as degraded freshness

calculateStatus() only downgrades recent data when healthStatus === 'COVERAGE_PARTIAL', so STALE_SEED is ignored when age is within the fresh window. /api/health can emit STALE_SEED without seedAgeMin (e.g. missing seed-meta), and recordSeedHealth() then preserves the prior lastUpdate; that combination leaves the source marked fresh even though backend health explicitly says the seed is stale. This causes false-green freshness in exactly the stale-seed failure mode this feature is meant to surface.

Useful? React with 👍 / 👎.

Comment on lines +171 to +173
await refreshDataFreshnessFromHealth({ signal: this.signal });
this.lastHealthFreshnessRefreshAt = Date.now();
} catch (error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Throttle health refresh attempts on failure

lastHealthFreshnessRefreshAt is only advanced after a successful health fetch. If /api/health is failing, each subsequent refresh() call retries immediately because the timestamp never moves, and this panel refreshes often via the dataFreshness subscription. That can create repeated concurrent requests and noisy logs during outages, adding avoidable load while the backend is already unhealthy.

Useful? React with 👍 / 👎.

@lspassos1
Copy link
Copy Markdown
Collaborator Author

Follow-up pushed in 88b32fc0 for the freshness outage review.

Changes:

  • /api/health top-level REDIS_DOWN or REDIS_PARTIAL without per-check details now marks every mapped source unhealthy instead of applying zero updates.
  • COVERAGE_PARTIAL no longer renders as fresh; recent partial coverage is treated as stale.
  • StrategicRiskPanel now updates the health refresh throttle only after a successful fetch, so failed health calls do not block retries for 60s.
  • Removed the redundant outer background .catch() around the health freshness refresh.
  • Added regression tests for top-level Redis outage payloads and partial coverage.

Validation:

  • npx tsx --test tests/data-freshness-health.test.mts
  • npm run typecheck
  • npx biome lint src/services/health-freshness.ts src/services/data-freshness.ts src/components/StrategicRiskPanel.ts tests/data-freshness-health.test.mts
  • git diff --check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant