Skip to content

Aswincloud/status-page

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

18 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🟒 aswincloud status

A self-hosted, BetterStack-style live status page for a home network.

Built on the Cloudflare edge so the page stays up even when home β€” or the prober β€” is down.

β–Ά Live demo: status.aswincloud.com

Deploy Database Prober CI Free tier Deps


How it works

A status page that lives at home dies with home β€” useless. So the page runs on Cloudflare's global edge (a Worker + a D1 SQLite database), and an external always-on server runs a tiny Docker prober that checks your home and pushes heartbeats in.

 β”Œβ”€ external always-on server ─┐         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Cloudflare edge ───────────┐
 β”‚  prober (Docker)            β”‚         β”‚  Worker  Β·  D1  Β·  per-minute cron      β”‚
 β”‚   every 30s:                β”‚  HTTPS  β”‚                                         β”‚
 β”‚     check home  ───────────────────▢  β”‚  POST /api/ingest   β†’ record + detect   β”‚
 β”‚     POST heartbeat          β”‚ (Bearer)β”‚                       up/down flips     β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚  GET  /api/status   β†’ JSON for the page β”‚
                                         β”‚  scheduled()        β†’ watchdog + prune  β”‚
   visitor ─── GET / ─────────────────▢  β”‚  static assets      β†’ the status page   β”‚
                                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                        β”‚ on a down/up flip
                                                        β–Ό
                                            πŸ“§ Email Β· πŸ’¬ Slack Β· ✈ Telegram

Both failure modes surface correctly:

What breaks How it's caught
🏠 Home is down The prober's check fails β†’ pushes down β†’ recorded, incident opened, alerts fire.
πŸ–₯️ The prober/server itself dies No heartbeats arrive β†’ the Worker's per-minute cron watchdog sees stale data (>2 min) and records a synthetic outage. No silent green.

A note on the check type. The demo monitors torrent.aswincloud.com, which is proxied through Cloudflare. A ping to it would only reach Cloudflare's edge (always up) and show false-green β€” so the monitor uses an HTTP check that flows through Cloudflare to the home origin: 200 when home is up, 52x when it isn't. For a direct (non-proxied) host, ping or tcp work too. The prober supports all three.


✨ Features

  • Overall banner β€” All systems operational Β· Partial outage Β· Major outage
  • Per-monitor cards with a live status pill
  • 90-day uptime bar β€” hover any day for that day's uptime %
  • Uptime % over 24h / 7d / 30d
  • Response-time chart (last 2 hours, SVG, breaks the line on downtime)
  • Internet speed graph β€” real download/upload Mbps over time (current Β· avg Β· peak), measured at home by a separate speed agent so it reflects your actual connection
  • Incident timeline β€” ongoing + resolved, with durations
  • On-demand "Test now" β€” owner-only button triggers a speed test instantly over a WebSocket push; gated by Google sign-in (OWNER_EMAIL allow-list)
  • Alerts on every down/recovery β€” Email (Resend), Slack, and Telegram, each independent
  • Dark / light theme toggle Β· fully responsive Β· zero frontend dependencies
  • Config-driven β€” add a monitor by editing one JSON file; the page auto-discovers it

Supported check types: http Β· tcp Β· ping.


πŸ—‚οΈ Layout

wrangler.jsonc     Worker + D1 + cron + static-assets + custom-domain config
schema.sql         D1 tables + starter monitor seed
src/
  index.ts         fetch() router + scheduled() cron watchdog
  api.ts           /api/ingest (auth) + /api/status (edge-cached JSON)
  db.ts            D1 helpers + Env types + tunables (STALE_MS, RETENTION_MS)
  stats.ts         uptime %, 90-day buckets, latency series, incidents
  alerts.ts        email / Slack / Telegram β€” each activates only when its secrets exist
  auth.ts          Google OIDC sign-in + signed session cookie (WebCrypto)
  agent-link.ts    Durable Object holding the speed agent's WebSocket (on-demand push)
public/            The status page β€” index.html Β· styles.css Β· app.js
prober/            Docker prober β€” reachability checks (runs OUTSIDE home)
speedagent/        Docker speed agent β€” Ookla speed tests (runs AT home)

πŸš€ Part 1 β€” Deploy the page (Cloudflare)

Requires a Cloudflare account. For the custom domain, your zone should be on Cloudflare.

npm install

# 1. Create the D1 database, then paste the printed database_id into wrangler.jsonc
npx wrangler d1 create status-db

# 2. Create the tables (remote = the real database the Worker uses)
npx wrangler d1 execute status-db --file schema.sql --remote

# 3. Set the shared ingest secret β€” SAVE it, the prober needs the same value
#       openssl rand -hex 32
npx wrangler secret put INGEST_TOKEN

# 4. Deploy
npx wrangler deploy

Custom domain β†’ in wrangler.jsonc the routes block maps status.aswincloud.com; since the zone is on Cloudflare, DNS + TLS are provisioned automatically on deploy. Before a domain is attached, the Worker is live at home-status.<account>.workers.dev.


🐳 Part 2 β€” Run the prober (external, always-on server)

Copy the prober/ folder to that server, then:

cd prober
cp .env.example .env
nano .env                       # INGEST_TOKEN = the same value from step 3 above

# config.json already points at https://status.aswincloud.com/api/ingest
docker compose up -d
docker compose logs -f          # expect: "ok β€” home-network:140ms"

restart: unless-stopped + Docker-on-boot keep it running across reboots and crashes.

Reachability (the prober) and speed (the agent below) run on different boxes on purpose. The prober must be outside home to tell when home is down; the speed agent must be inside home to measure your real connection.


πŸ“Ά Part 3 β€” Run the speed agent (at home)

Measures real home download/upload with the official Ookla CLI and pushes every 15 min. Run it on a machine at home (the one whose internet you want to graph):

cd speedagent
cp .env.example .env
nano .env                       # INGEST_TOKEN = the same value as everything else
docker compose up -d
docker compose logs -f          # expect: "ok β€” ↓329.8 ↑333.4 Mbps Β· ping 6.8ms Β· BSNL"

Each test uses real bandwidth (~25 MB down + ~10 MB up). Change the cadence with INTERVAL_SECONDS in docker-compose.yml. The image auto-detects x86_64 / arm64.

The agent also keeps a persistent outbound WebSocket to the Worker (held by a Durable Object) so an owner can trigger an on-demand test with no polling β€” see the next section.


⚑ On-demand "Test now" (owner only)

An owner-only button in the speed panel triggers a test immediately: the Worker pushes {cmd:'run'} down the agent's WebSocket β†’ the agent runs a test β†’ the result appears in ~30s. A server-side 2-minute cooldown prevents abuse. The page itself stays fully public; only the action is gated β€” by Google sign-in:

Method How it works
Sign in (UI) The Sign in button (top-right) β†’ Sign in with Google. Only an address in OWNER_EMAIL is accepted; sets a signed session cookie. The button then reads Sign out.
Control token Server-side fallback for curl/cron only β€” send Authorization: Bearer <CONTROL_TOKEN> to /api/request-test. Not exposed in the UI.

Secrets (all Cloudflare Worker secrets, never in the repo):

npx wrangler secret put SESSION_SECRET     # HMAC key for the session cookie
npx wrangler secret put OWNER_EMAIL        # comma-separated allow-list: a@x.com,b@gmail.com
npx wrangler secret put GOOGLE_CLIENT_ID
npx wrangler secret put GOOGLE_CLIENT_SECRET
npx wrangler secret put CONTROL_TOKEN      # optional β€” server-side curl/cron fallback

For Google sign-in, create an OAuth Web application client and set its redirect URI to https://status.aswincloud.com/api/auth/callback (scopes openid email). OWNER_EMAIL is a comma-separated allow-list, so more than one Google account can be an owner. A failed callback (wrong account, expired state) redirects back to the page with a dismissible banner + Try another account β€” never a dead-end error page.


βœ… Verify end-to-end

  1. Open the page β€” the monitor turns green within ~30s; sparkline + 90-day bar fill in.
  2. curl -s https://status.aswincloud.com/api/status | jq '.overall, .monitors[0].up' β†’ "operational", true.
  3. Outage test: docker compose stop the prober. Within ~2 min the card flips red, an incident opens, alerts fire. docker compose start β†’ it recovers and the incident shows resolved with a duration.

βž• Add another monitor

Edit prober/config.json, then docker compose restart. The new card appears automatically.

{
  "monitors": [
    { "id": "home-network", "name": "Home Network", "type": "http", "target": "https://torrent.aswincloud.com" },
    { "id": "jellyfin",     "name": "Jellyfin",     "type": "tcp",  "target": "192.168.1.50:8096" },
    { "id": "router",       "name": "Router",       "type": "ping", "target": "192.168.1.1" }
  ]
}

ping/tcp to LAN addresses require the prober to have a network route to them. To delete a monitor's history immediately: npx wrangler d1 execute status-db --remote --command "DELETE FROM monitors WHERE id='jellyfin'; DELETE FROM checks WHERE monitor_id='jellyfin';"


πŸ”” Alerts

src/alerts.ts fires on every down/recovery transition. Each channel activates only when its secrets are present β€” run any combination, or none. Secrets live in Cloudflare, never in this repo.

πŸ“§ Email (Resend)

npx wrangler secret put RESEND_API_KEY   # resend.com β€” sending domain must be verified
npx wrangler secret put ALERT_FROM       # "aswincloud status <status@aswincloud.com>"
npx wrangler secret put ALERT_TO         # aswin@aswincloud.com  (comma-separated for several)

πŸ’¬ Slack β€” create an app with the chat:write scope, /invite the bot to a channel, grab the channel ID (C0…):

npx wrangler secret put SLACK_BOT_TOKEN
npx wrangler secret put SLACK_CHANNEL

✈ Telegram β€” create a bot via @BotFather, read your chat id from https://api.telegram.org/bot<TOKEN>/getUpdates:

npx wrangler secret put TELEGRAM_BOT_TOKEN
npx wrangler secret put TELEGRAM_CHAT_ID

After setting secrets, just push to main (or npx wrangler deploy) β€” no code change.


πŸ”„ Continuous deployment

Connected to Cloudflare Workers Builds: every push to main runs npx wrangler deploy; pushes to other branches run npx wrangler versions upload (preview). Worker secrets persist across builds.

The prober is not part of this pipeline β€” it's a container on an external box. Update it there with git pull && docker compose up -d --build.


πŸ§ͺ Local development

npx wrangler dev                                            # local Worker + D1 at :8787
npx wrangler d1 execute status-db --file schema.sql --local # seed the local DB
cd prober && INGEST_TOKEN=devtoken INGEST_URL=http://localhost:8787/api/ingest node prober.js
# (give wrangler dev the matching token via a .dev.vars file: INGEST_TOKEN="devtoken")

Open http://localhost:8787.


πŸ“ Notes & limits

  • Fits comfortably in Cloudflare's free tier (Workers + D1 + cron).
  • Raw checks are pruned after 90 days; uptime % and the day-bar use indexed aggregates, and /api/status is edge-cached ~15s so page loads never re-scan.
  • Tunables: STALE_MS (2 min) and RETENTION_MS (90 d) in src/db.ts; check interval in prober/config.json (intervalSeconds).
  • Secrets (INGEST_TOKEN, alert credentials) are Cloudflare Worker secrets β€” never committed.

Built with Cloudflare Workers Β· D1 Β· Docker β€” and zero frontend dependencies.

About

🟒 Self-hosted, BetterStack-style live status page for a home network β€” Cloudflare Worker + D1 + a Docker prober, with email & Slack alerts. Runs on the free tier.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors