-
Notifications
You must be signed in to change notification settings - Fork 0
Site Status Page
The Site status page at /status.html shows the live health of every Supabase edge function this site uses. It also carries an announcement feed pulled from GitHub issues labeled incident, and a small colored dot in the topbar summarizes the overall state so a reader knows something is off before they even click through.
Filed as issue #254. The check moved off a GitHub Actions cron to a Cloudflare Worker in #275. This page documents the moving parts so future changes can be made safely.
The health check runs on a Cloudflare Worker called pp-edge-status, not GitHub Actions. GitHub's schedule: cron was best-effort: runs drifted 15 to 45+ minutes under load, got skipped, and got auto-disabled after 60 days of repo inactivity. It also only wrote edge-status.json to the prod gh-pages branch, so the staging status page had no live data. A Cloudflare Cron Trigger fires on time, and one CORS endpoint feeds both prod and staging.
The old .github/workflows/edge-fn-health.yml (running scripts/check-edge-fn-health.sh) stays as a manual workflow_dispatch backstop that still writes the static edge-status.json. Its schedule: block is removed once the worker heartbeat is confirmed in prod. The bash script's FNS=() array is the source of truth the worker's FNS list is kept in sync with (a test enforces parity).
| Thing | Path |
|---|---|
| Page shell | status.html |
| Frontend logic | js/status/main.js |
| Frontend styles | css/status/status.css |
| Worker (probe + serve) | workers/edge-status/index.js |
| Worker config (cron, KV, vars) | workers/edge-status/wrangler.toml |
| Worker deploy target |
make deploy-worker -> scripts/deploy-worker.sh
|
| Worker tests | tests/edgeStatusWorker.test.js |
| Live endpoint constant |
EDGE_STATUS_ENDPOINT in js/status/main.js
|
| Backstop health script | scripts/check-edge-fn-health.sh |
| Backstop cron workflow |
.github/workflows/edge-fn-health.yml (dispatch-only) |
| Discord incident notifier | .github/workflows/discord-incidents.yml |
| Static fallback data |
edge-status.json (on gh-pages) |
| Topbar dot markup + fetch |
js/lib/topbar.js (wireStatusDot) |
| Topbar dot styles | css/shared/topbar.css |
- The worker's
scheduled()handler fires on a real*/15 * * * *Cron Trigger ([triggers] cronsinwrangler.toml). - It pings every tracked Supabase edge function with an OPTIONS request (the CORS preflight, which every deployed function answers quickly regardless of auth state).
- For each function it records: response HTTP status, latency in ms, an ISO timestamp, and a derived status (
operational/degraded/down). - It writes the aggregated payload (same shape as the old
edge-status.json) to Workers KV under theedge-statuskey, and appends each function's latency onto a rolling 7-day history underedge-status-history. - The worker's
fetch()handler serves that payload as JSON with CORS for the site's real origins:https://www.proton-pulse.com(prod),https://staging.proton-pulse.com(CF Pages staging), andhttps://mdeguzis.github.io(retired GH Pages staging kept for rollback). If a request comes in from an origin not on the list the worker falls back to the first entry (prod), which is what caused the "JSON.parse: unexpected character" error on staging after the CF Pages migration until #362 added the new origin. Regression test intests/edgeStatusWorker.test.jspins the full list.
The worker needs one plain var and one secret (set in wrangler.toml / wrangler secret put):
-
SUPABASE_URL(plain[vars]entry) -
SUPABASE_ANON_KEY(secret)
It also binds a KV namespace EDGE_STATUS_KV (id in wrangler.toml). If moving to a different Supabase project, update the var/secret on the worker, not this file.
pp-edge-status is deployed at https://pp-edge-status.mdeguzis.workers.dev. Routes:
| Request | Behavior |
|---|---|
GET / |
Returns the latest snapshot from KV. Cold KV (fresh deploy) runs one live probe so the page is never blank. |
GET /?history=<fn> |
Returns that function's [[epochSec, ms], ...] latency series for the sparkline. ?history=1 or ?history=all returns every series. |
POST / |
Admin-only "Check now" (see below). |
OPTIONS / |
CORS preflight. |
EDGE_STATUS_ENDPOINT in js/status/main.js holds the worker URL. If it is '', the page falls back to the static edge-status.json, so the page keeps working before the worker is wired in or if the worker is unreachable.
| HTTP code | Derived state | Rationale |
|---|---|---|
| 200, 204 | operational | Function answered the preflight normally. |
| 401, 403 | operational | Function is up and responded fast, it just rejected the anonymous probe based on its auth policy. Real signed-in traffic goes through. This is intentional for steam-callback and any function that requires a session. |
| 5xx | down | Server-side error, function crashed or Supabase misbehaving. |
| 000 (curl connection failure) | down | Never got a response, DNS or network. |
| everything else | degraded | Not clearly good or bad, worth eyeballing. |
overall is the worst of the individual states: any down makes overall down; else any degraded makes overall degraded; else operational.
The list is hard-coded as FNS in workers/edge-status/index.js (and mirrored in scripts/check-edge-fn-health.sh). When adding a new edge function under supabase/functions/, add its name to both (see "Adding a new service to track"). The order in the array is the order the cards render in.
Current set:
image-refetchplugin-link-completeplugin-link-removeplugin-link-startplugin-link-statusplugin-link-unlinkplugin-links-listprotondb-summarysteam-appdetailssteam-callbacksteam-depot-infosteam-exploresteam-library-lookupsteam-newssync-steam-libraryuser-system-upload
Each function's source: supabase/functions/<name>/index.ts.
js/status/main.js boots on page load and does two things in parallel:
-
Service list:
fetchStatusPayload()triesEDGE_STATUS_ENDPOINT(the worker) first and falls back to the staticedge-status.jsonviadataUrl()if the worker is unset or unreachable. Renders one clickable card per service, auto-refreshing every 60 seconds. -
Announcements: fetches the public GitHub issues API for the repo, filtered to
label=incident&state=all, unauthenticated. Renders open incidents first (yellow left border), then resolved ones (muted). Also auto-refreshes every 5 minutes.
Clicking a service card opens a modal (see openServiceModal in main.js). The modal has:
- An explainer at the top that varies by HTTP code.
explainerFor(svc)is the switch that picks the right copy for the code. - A definition list of the raw fields.
- An admin-only Check now button (super admins only, see below).
- A
<pre>block with the full raw JSON blob for the service. - A latency sparkline under the JSON.
The modal closes on the X button, an outside click, or Escape.
When a tile modal opens, fetchHistory(fn) pulls that function's [[epochSec, ms], ...] series from the worker (?history=<fn>) and renderSparkline() draws a small time-scaled inline SVG under the raw JSON, with min / avg / max labels. No chart library. The series starts empty on a fresh worker deploy and fills as crons run (a super admin can seed points instantly with Check now). Shows a "still collecting" message until there are at least two points.
Super admins get a Check now button in each service tile that re-probes that one function on demand instead of waiting for the next cron. detectSuperAdmin() gates whether the button renders (reads the caller's admins row filtered to role=eq.super_admin). The button is not the security boundary: checkServiceNow() POSTs the user's Supabase access token to the worker, which re-verifies super_admin server-side (verifySuperAdmin: resolve the token via /auth/v1/user, then read the caller's admins row) before running any probe. Unauthenticated or non-admin callers get 401/403 and no probe runs. A successful check merges the fresh result into KV, appends a history point, and returns the full payload so the page and modal re-render.
make deploy-worker # deploys workers/edge-status (default)
make deploy-worker WORKER=<name> # any other folder-based worker under workers/make deploy-worker calls scripts/deploy-worker.sh, which validates the worker dir + wrangler.toml and runs npx wrangler deploy. It relies on a prior npx wrangler login. One-time setup for a fresh worker (KV namespace, SUPABASE_ANON_KEY secret) is documented in the header of workers/edge-status/index.js.
Every page loads topbar.js, which injects the Status nav link and calls wireStatusDot. That helper:
- Reads a sessionStorage cache under
pp-edge-status. If less than 60 seconds old, applies the cached color and skips the network. - Otherwise
fetch('edge-status.json', { cache: 'no-store' }), readsoverall, and applies it to every.topbar-status-doton the page. - Writes the new state to sessionStorage with a fresh timestamp.
The dot uses data-state="operational|degraded|down|unknown" to pick its color. See the .topbar-status-dot[data-state=...] rules in css/shared/topbar.css.
.github/workflows/discord-incidents.yml fires on the issues event when an issue labeled incident is opened, closed, reopened, labeled, or unlabeled. It filters in the if: clause so non-incident issues exit early without spinning up a runner.
Posts to the DISCORD_WEBHOOK_ANNOUNCE webhook with a short plain-text message + URL. If the secret is not set the workflow logs a message and exits 0 without posting.
Message format:
[INCIDENT] New announcement: #123
The title of the issue
https://github.com/mdeguzis/proton-pulse-web/issues/123
_by mdeguzis_
Actions covered: opened, labeled (added incident label), closed, reopened, unlabeled (incident label removed).
To raise an incident from the site's status page, click the Report an issue button in the Announcements block. It deep-links to a new issue with the incident label already applied.
Directly:
gh issue create --repo mdeguzis/proton-pulse-web \
--label incident \
--title "Incident: <what>" \
--body "<summary and impact>"When the incident is resolved, close the issue. The status page and Discord announcement will both reflect the resolved state on their next refresh.
The function list lives in two places that a test (tests/edgeStatusWorker.test.js) keeps in parity:
- Add the function name to the
FNSarray inworkers/edge-status/index.js. - Add the same name to the
FNS=()array inscripts/check-edge-fn-health.sh(the backstop). - Run
make deploy-workerso the live worker picks it up on the next cron. - Commit + push both files. The parity test fails if the two lists diverge.
Status page shows "Could not load status data":
- The worker is unreachable AND the static
edge-status.jsonfallback is missing. Hithttps://pp-edge-status.mdeguzis.workers.dev/directly: a 200 with JSON means the worker is fine and the problem is the page's fetch. A worker error usually means a bad deploy, a missing KV binding, or an unsetSUPABASE_ANON_KEYsecret. Re-runmake deploy-workerand confirm the KV id + secret.
Sparkline says "still collecting" forever:
- History only accumulates from probes run under the current worker deploy. Confirm
GET /?history=<fn>returns a non-empty array. If it's empty, the cron may not have fired yet (wait 15 min) or a super admin can seed points with Check now.
One function is always "degraded":
- Check the HTTP code in the detail modal. If it's 401 or 403, it should be operational, not degraded. Confirm the case statement in the script covers that code.
Topbar dot never turns green:
- SessionStorage cache may be sticky. Open devtools, delete the
pp-edge-statuskey, reload. If that fixes it, look for a stale write pattern inwireStatusDot.
Discord notification did not fire on an incident:
- Confirm the issue actually carries the
incidentlabel at the moment the event fired. Label additions to existing issues fire withaction: labeled, which the workflow handles. - Confirm
DISCORD_WEBHOOK_ANNOUNCEis set in the repo secrets.
- Web Deploy Workflow - how staging vs prod diverge for data files
- Secrets and Environment - what each secret unlocks
- API Reference - Supabase edge function contracts