Skip to content

feat(core): add a "not checked yet" state to the public status contract - #109

Merged
amondnet merged 4 commits into
mainfrom
amondnet/103-not-checked-yet-state
Oct 22, 2026
Merged

amondnet merged 4 commits into
mainfrom
amondnet/103-not-checked-yet-state

Conversation

@amondnet

@amondnet amondnet commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A configured site with no real result and no previous summary was served as down, so the public page and badges showed an outage nobody observed. This happened for a new EmDash plugin monitor until its shard first ran, and in both distributions for a webhook-only Sentry monitor until its first event.

This PR adds an explicit "not checked yet" state to the public contract:

  • Contract (@statusbeam/core/contract). SiteSummary, the sites[] entries of statusResponseSchema, and siteResponseSchema gain an optional boolean pending. A site that has not been checked yet is served with "pending": true next to a placeholder "status": "down". For every other site the key is omitted.
  • Rollup. buildSiteSummaries marks a site pending when it has no result and no previous summary. It stays pending while its previous summary is pending, and its first real result clears it. The plugin's summariesFromSnapshot gets this through core, so standalone and the plugin produce the same bodies.
  • Overall severity. The new overallSiteSeverity skips pending sites; when every site is pending the result is operational. buildStatusResponse, overallBadge and the apps/web page banner all use it.
  • Change detection. detectStatusChanges does not compare a result against a pending summary, so the first real result sends no notification and does not purge the cache. This matters in the standalone worker ingest path, which reads the previous SiteSummary from KV. The plugin's run and webhook paths compare against real results only, so they already behaved this way; new tests now cover both.
  • Badges. The site status, uptime and response-time badges for a pending site show not checked in lightgrey, not down in red or 100% in green.
  • Renderers. @statusbeam/emdash-ui SiteDetail and apps/web StatusList show a pending site with a text label ("Not checked yet"), a clock icon (emdash-ui), and a neutral tone, so color is never the only signal (NFR-4). A new status.notChecked i18n entry has translations for every locale.
  • Docs. The contract doc comments, the README "Badges & public API" section, and the EmDash guide's public endpoints section are updated.

Related issue

Closes #103

Backward compatibility: why pending and not a new status value

CONTRACT_VERSION stays 1. Existing consumers (the published @statusbeam/emdash-ui and apps/web) parse bodies with zod objects. These strip unknown keys but treat status as the closed enum up | degraded | down. A new value such as "not_checked" would fail that enum, and the old client would then report the whole source as unavailable. An optional pending key is stripped silently, so an old consumer reads "down" exactly as it does today, and a new consumer reads the new state. Because the key is omitted for checked sites, bodies for checked sites stay byte-identical to the current ones. A contract test pins this with a pre-pending schema.

Tests

Behavior Test
No result and no previous summary → pending + placeholder down packages/core/src/rollup.test.ts › buildSiteSummaries pending state
Stays pending until a real result arrives, then cleared; key omitted for checked sites; non-pending previous kept same describe
Contract serves and accepts pending; overall ignores pending (all pending → operational) packages/core/src/contract.test.ts › a site not checked yet
A pre-pending consumer schema still parses the body as down same describe
detectStatusChanges ignores a pending prior packages/core/src/rollup.test.ts › detectStatusChanges
Worker ingest: first real result after pending → no notify or purge, pending cleared, untouched site pending; a real down → up still notifies apps/worker/src/sentry-webhook.handler.test.ts
Plugin run: unprobed monitor reads pending; first result notifies nobody packages/emdash-plugin/tests/notify.test.ts › status-change notifications
Plugin webhook: webhook-only Sentry monitor pending until first event, which notifies nobody packages/emdash-plugin/tests/webhooks.test.ts › an accepted webhook event
Unprobed monitor reads pending (updated expectation, was down) packages/emdash-plugin/tests/notify.test.ts › leaves a monitor whose only probes hit the limit out…
Badges: not checked/lightgrey; overall badge ignores pending packages/core/src/badge.test.ts › badges for a site not checked yet
Parity: plugin and standalone bodies identical for a pending monitor across every route packages/emdash-plugin/tests/public.host.ts › serves a monitor not checked yet as pending…
i18n label exists in every locale and differs from every severity label packages/core/src/i18n.test.ts

Neither @statusbeam/emdash-ui nor apps/web has component render tests, so the renderer changes are covered by astro check and typecheck only.

Gate results

  • bun run build: 7/7 tasks successful
  • bun run typecheck: 11/11 tasks successful
  • bun run lint: eslint . clean
  • bun test: 513 pass, 0 fail (36 files)
  • bun run --cwd packages/emdash-plugin test:host: 4 files, 28 tests passed

Checklist

  • PR title follows Conventional Commits
  • Tests added or updated, and the suite passes (bun run test)
  • Lint/format pass (bun run lint)
  • Documentation updated if behavior changed
  • No breaking change

Summary by cubic

Adds a "not checked yet" state to the public status contract so configured sites with no real check result are no longer served as down. New EmDash monitors before their shard first runs, and webhook-only Sentry monitors before their first event, now read pending: true beside the placeholder down, and public pages and badges show "Not checked yet" instead of a phantom outage.

The change stays on CONTRACT_VERSION 1: old consumers strip the unknown key and read down as before, while a new status enum value would fail their closed schemas. The schema accepts only pending: true, never false, and the key is omitted for checked sites, so existing bodies are byte-identical.

Contract and rollup

  • buildSiteSummaries marks a site pending while it has no result and no previous summary; a real result clears it, and the plugin's summariesFromSnapshot goes through core so both distributions serve identical bodies.
  • The worker ingest marks pre-upgrade snapshots that stored a never-checked site as an unflagged down/0 as pending when the site has no checks row, so its first real result after an upgrade is not reported as a change.
  • overallSiteSeverity skips pending sites in the aggregate status, overall badge, and apps/web banner; all-pending resolves to operational.
  • detectStatusChanges ignores pending prior summaries, so the first real result sends no notification and does not purge the cache.

Rendering and badges

  • Site, uptime, and response-time badges read not checked in lightgrey.
  • @statusbeam/emdash-ui SiteDetail and apps/web StatusList show a "Not checked yet" label, clock icon, and neutral tone, so color is never the only cue; a pending site's uptime windows read — instead of 100%.
  • A new uptimeFigure helper drives every uptime figure: pending or all-no-data histories yield none, so the row header, its breakdown, and both 90-day timelines agree on the visible value and its accessible name (timeline.ariaNoData), never announcing "100%" for an empty window.
  • A new status.notChecked i18n entry covers every locale, and the README and EmDash guide document the state.
  • Sonar analysis now treats the emdash-plugin *.host.ts suites as tests so the new parity coverage in public.host.ts no longer counts against the new-code gate.

Written for commit adcca62. Summary will update on new commits.

Turn on auto-fix

A configured site with no real result and no previous summary used to be
served as `down`, showing an outage nobody observed (a new plugin monitor
before its shard runs, a webhook-only Sentry monitor before its first event).

The contract now marks such a site with an optional `pending: true` beside a
placeholder `status: "down"`, staying on CONTRACT_VERSION 1: consumers that
predate the field strip the unknown key and read `down` as before, whereas a
new `status` enum value would fail their schema. The key is omitted for
checked sites, so existing bodies are byte-identical.

- buildSiteSummaries marks the site pending until its first real result
- the overall severity and overall badge ignore pending sites
- detectStatusChanges does not compare against a pending summary, so the
  first real result sends no notification (worker ingest; the plugin never
  had a prior result)
- site, uptime and response-time badges read "not checked" in grey
- emdash-ui SiteDetail and apps/web StatusList label the site
  "Not checked yet" (new i18n `status.notChecked` in every locale)

Closes #103

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a "pending" state for monitors that have not been checked yet, ensuring they are styled neutrally as "Not checked yet" rather than showing an outage. It updates the core status rollup, badge generation, and notification logic to ignore pending sites, and adds comprehensive test coverage across the packages. The reviewer pointed out a medium-severity issue where a pending site might misleadingly display a "100%" 90-day uptime due to an empty history, suggesting it be set to "—" instead.

Comment thread packages/emdash-ui/src/components/SiteDetail.astro Outdated
@codecov

codecov Bot commented Oct 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

The workerd host suites are test files, but only *.test.ts and *.spec.ts
were carved out of sources, so new lines in public.host.ts counted as
uncovered source and failed the new-code coverage gate.
@amondnet
amondnet marked this pull request as ready for review October 22, 2026 19:33
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T19:37:16.590973Z 4207aa5 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@greptile-apps

greptile-apps Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issues were found.

Summary

Adds pending: true for sites that have never been checked, without changing the existing status enum.

  • Sites without a real check result are marked as pending.
  • A first real check no longer triggers a status-change notice.
  • Overall status ignores sites that have not been checked yet.
  • Badges label unchecked sites instead of showing fake results.
  • Status pages give unchecked sites a neutral label and no uptime figure.
  • Sonar treats host integration suites as tests.

Reviews (3) · Last reviewed commit: "fix(core): name a timeline with no uptim..." · Reviewed by Greptile

Comment thread packages/core/src/rollup.ts

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

Copy link
Copy Markdown

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: 4207aa58a1

ℹ️ 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 packages/core/src/rollup.ts
Comment thread apps/web/src/components/StatusList.tsx

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

cubic analysis

All reported issues were addressed across 22 files

Linked issue analysis

Linked issue: pleaseai/statusbeam#103: feat(core): add a "not checked yet" state to the public status contract

Status Acceptance criteria Notes
✅ Serve unchecked monitors as pending with matching standalone and plugin bodies. Core marks monitors without results pending, and route tests compare plugin responses with standalone responses.
✅ Exclude pending monitors from the overall status. overallSiteSeverity skips pending sites, and contract tests cover mixed and all-pending summaries.
✅ Show a text label for pending status in both renderers. SiteDetail and StatusList render the translated “Not checked yet” label with neutral styling.
✅ Update the contract compatibly for existing consumers. The optional pending key preserves version 1, and a legacy-schema test confirms old consumers still parse it.
✅ Update the plugin test for an unprobed monitor. The existing expectation now checks that an unprobed monitor has pending true beside its placeholder down status.
Architecture diagram
sequenceDiagram
    participant CB as Check Badge
    participant CBH as Check Badge Handler
    participant WFE as Worker Fetcher
    participant WEE as Worker Entrypoint
    participant CV as Cloudflare Variant
    participant WR as Worker Runtime
    participant FA as Fetcher Adapter
    participant S3 as S3 Bucket
    participant D1 as D1 Database
    participant RR as Rollup Routine
    participant KVR as KV Read
    participant R2A as R2 Access
    participant SA as Static Assets
    participant KVW as KV Write
    participant F as Fetcher
    participant R2C as R2 Cache
    participant R as Runtime
    participant I as Isolate

    Note over CB,CBH: Check Badge Handler
    CB->>CBH: Evaluate Worker Fetch
    CBH->>WFE: Handle Worker Fetch
    Note over WFE,WEE: Worker Fetcher
    WFE->>WEE: Evaluate Entrypoint
    Note over WEE,CV: Worker Entrypoint
    WEE->>CV: Convert to Cloudflare
    CV->>WR: Start Runtime
    WR->>FA: Wrap Fetcher
    Note over FA,S3: Fetcher Adapter
    FA->>S3: Upload to S3
    S3-->>FA: S3 Result
    Note over FA,RR: Fetcher Adapter
    FA->>RR: Upload to Rollup
    Note over RR,D1: Rollup Routine
    RR->>D1: Query Database
    D1-->>RR: Database Result
    RR->>KVR: Read from KV
    KVR-->>RR: KV Value
    RR->>R2A: Access R2
    R2A-->>RR: R2 Value
    Note over RR,SA: Rollup Routine
    RR->>SA: Read Static Assets
    RR->>KVW: Write to KV
    Note over KVW,R2C: KV Write
    KVW->>R2C: Cache in R2
    Note over R2C,F: R2 Cache
    R2C->>F: Fetch Data
    Note over F,R: Fetcher
    F->>R: Run
    Note over R,I: Runtime
    R->>I: Start Isolate
    I-->>R: Isolate Result
    R-->>F: Runtime Result
    F-->>R2C: Fetch Result
    R2C-->>KVW: Cache Result
    KVW-->>RR: Write Result
    RR-->>FA: Rollup Result
    FA-->>WR: Adapter Result
    WR-->>CV: Runtime Result
    CV-->>WEE: Cloudflare Result
    WEE-->>WFE: Entrypoint Result
    WFE-->>CBH: Worker Result
    CBH-->>CB: Handle Result
Loading

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread packages/core/src/types.ts Outdated
Comment thread packages/core/src/rollup.ts
… uptime

A snapshot written before `pending` existed stored a never-checked site as
an unflagged `down`/`0`. After an upgrade, a webhook-only Sentry monitor kept
that false outage until its first event. The standalone ingest now marks
such an entry pending when the site has no row in `checks` at all, before
change detection runs, so its first real result is not reported as a change.
D1 never deletes check rows, so a real `down` keeps its status however old
it is.

The renderers showed a pending site's empty history as 100% uptime. The
emdash-ui SiteDetail and apps/web StatusList now show an em dash for every
uptime window of a pending site, as the uptime badge already does.

`SiteSummary.pending` and the contract schema now accept only `true`, so a
checked site cannot publish `pending: false`.
@amondnet

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 7 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread apps/web/src/components/StatusList.tsx Outdated
The 90-day timelines in apps/web and emdash-ui built their accessible name
from the window uptime, which reads an empty or all-no-data history as 100%,
so a site not checked yet was announced as "100% uptime".

A new core helper, `uptimeFigure`, returns the figure a renderer shows, or
`null` for a pending site or a window with no checked day. The row header,
its breakdown and both timelines now use it, so the visible figure and the
accessible name always agree. A timeline with no figure is named by the new
`timeline.ariaNoData` label, translated in every locale.
@sonarqubecloud

Copy link
Copy Markdown

@amondnet
amondnet merged commit 790a7ec into main Oct 22, 2026
14 of 15 checks passed
@amondnet
amondnet deleted the amondnet/103-not-checked-yet-state branch October 22, 2026 21:57
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.

feat(core): add a "not checked yet" state to the public status contract

1 participant