Sign inSign up

shounak6942/rxkite-status

By shounak6942

•Updated 12 minutes ago

RxKite Status monitor: private admin, Gotify/email alerts, R2 checks. amd64/arm64.

Image
0

247

shounak6942/rxkite-status repository overview

⁠RxKite Status

An independent, self-hosted status page for RxKite. A Rust service on a Raspberry Pi checks application servers and storage, keeps history in SQLite, and publishes a public snapshot to Cloudflare. The public page continues serving the last snapshot if the Pi or RxKite goes offline.

Start here: step-by-step deployment guide⁠.

Public Cloudflare deployment: https://rxkite-status.pages.dev⁠. Pages, private R2 storage and the D1 queue are deployed. The page waits for the monitor's first snapshot; status.rxkite.in is pending DNS setup.

Docker Hub: shounak6942/rxkite-status⁠. Compose pulls the versioned release without building or requiring a Dockerfile; run python3 scripts/init-env.py, docker compose pull, then docker compose up -d. To compile from source, use the separate build override⁠.

For Portainer's stack web editor, use docker-compose.portainer.yml⁠ and the Portainer steps⁠. It takes credentials from stack environment variables and needs no local files.

For Cloudflare dashboard drag-and-drop, use npm run build:pages and follow the Pages Direct Upload guide⁠. The ZIP includes the API as a bundled _worker.js; configure its R2/D1 bindings and secrets before connecting the Pi.

flowchart LR
  Pi[Home Pi: Rust monitor + private admin] -->|HTTP health checks| App[BOM-1 / AMS-1 / CCU-1 / public site]
  Pi -->|Authenticated storage check| Worker
  Pi --> SQLite[(SQLite volume)]
  Pi -->|Authenticated outbound publish| Worker[Cloudflare Worker + React static assets]
  Worker --> R2[(Private R2 snapshot)]
  Visitors[Status visitors] --> Worker
  Worker -->|Signup / explicit link action| D1[(D1 event queue)]
  Pi -->|Pull + acknowledge| D1
  Pi -->|Persistent delivery outbox| Email[SMTP / Resend]
  Email --> Visitors

The Pi needs no inbound public port. The private admin is published at 127.0.0.1:8088 by Compose, and opens directly without a login. Access it through SSH forwarding; private Host and same-origin checks protect browser API access. Public publishing uses a separate token. There is no public admin route.

⁠Features

  • HTTP checks with configurable intervals, timeouts, expected status and RFC 6901 JSON paths. RxKite defaults match dallergy/prescribe: /ok, /database, /databaseMs, /version.
  • Up to two retries before an outage is recorded or announced; recovery resolves the automatic incident. One failed attempt followed by success produces no outage email.
  • Persisted response time, database latency, DB state and version samples. 90-day uptime bars; 7-day hourly latency charts; separate derived database components for configured health checks.
  • Private admin forms for monitors, branding, templates, subscriber/email settings, publishing, cache TTL and alerts. An advanced JSON editor exposes every configuration field. Tokens/passwords are redacted on reads and encrypted in SQLite with AES-256-GCM.
  • Authenticated incident/maintenance creation and updates, append-only updates, full local history. The public snapshot includes the 200 announcements (active first, then most recent resolved); older history remains available through the paginated private API.
  • D1 queue for subscriptions behind NAT; double opt-in; 48-hour confirmation expiry; unsubscribe in every subscriber email; rate limits; durable, encrypted delivery outbox. Explicit POST actions prevent email scanners from activating links.
  • SMTP with required TLS (STARTTLS or implicit TLS), Resend HTTP sender, and optional Gotify, Telegram and admin email outage/recovery alerts.
  • Responsive React/Tailwind/shadcn UI, accessible chart table and keyboard tooltips, light/dark themes. Refresh failures preserve displayed data; snapshots older than 15 minutes display a stale-monitor warning.
  • Non-root Docker runtime, persistent volume, healthcheck, restart policy, and multi-platform build workflow for arm64/amd64.

⁠Local development

Requires Rust 1.90.0, Node 22+, Python 3, and optionally Docker with Compose/Buildx.

npm ci
cargo test --locked
npm run build
npm test
npx playwright install chromium
npm run test:e2e

Render the public page using committed sample data (does not contact production):

VITE_SAMPLE=true npm run dev -w web

For a fully offline Docker test using mock health endpoints:

docker build -t rxkite-status:local .
RXKITE_IMAGE=rxkite-status:local python3 scripts/docker-smoke.py
# Exercise the Portainer deployment format with the same offline checks:
RXKITE_IMAGE=rxkite-status:local RXKITE_COMPOSE_FILE=docker-compose.portainer.yml python3 scripts/docker-smoke.py

The smoke test first starts the image as UID 10001 with its bundled INITIAL_CONFIG, a read-only filesystem and no outbound network. It then uses an isolated Compose project, random temporary secrets, a mock health service, a temporary volume, a one-off check cycle, and the actual service. It tests readiness, authentication, incident creation/resolution, disk publication and persistence after restart, then removes only its own test resources. It does not send emails or check production servers.

Run the native monitor by creating a private local configuration and writable data directory:

python3 scripts/init-env.py
mkdir -p data
set -a; . ./.env; set +a
DATABASE_URL=sqlite://data/status.sqlite STATUS_OUTPUT=data/status.json cargo run --locked

INITIAL_CONFIG seeds the settings until the first admin save; thereafter SQLite settings are authoritative. --once executes enabled checks and publishes one snapshot, then exits. Check failures are represented in the snapshot rather than causing a process exit; publishing/database failures cause a nonzero exit. --healthcheck calls the running service's liveness endpoint.

For a native monitor with local mock health checks and retained development data, run cargo build --locked then python3 scripts/dev-monitor.py. It generates private development credentials under the ignored data/ directory; the admin opens directly through its loopback address. It binds admin to loopback, writes a disk snapshot, and does not contact production services.

For the Worker locally, build the frontend, apply D1 migrations with --local, set private worker/.dev.vars (see deployment guide), and use npm run dev -w worker. Vite proxies /api to the local Worker. Real Pi publishing requires HTTPS; the offline smoke test uses disk mode.

⁠Configuration reference

config/rxkite.example.json⁠ is the complete seed example. Placeholder regional app monitors are disabled until you replace their URLs. The R2 monitor is disabled until publishing is connected; select R2 connected storage and enable it. The public RxKite monitor is enabled. The CCU-1 address is a sample LAN address, not an assertion about your network.

SettingMeaning / default
monitors[].idStable letters/digits/hyphens ID; retained history is keyed by this ID
name, group, url, enabledDisplay labels, grouping and HTTP(S) health/test URL; use uncredentialed URLs
check_typehttp (default) for URL checks; r2 for private storage writes/reads using the connected Cloudflare backend
interval_secs180 for app checks, 300 for R2; range 30–86400
timeout_secs10; range 1–60; limits the whole request/body
retries, retry_delay_secs2 retries, 30 seconds apart; retries 0–2, delay 1–120
expected_statusExact expected HTTP code, default 200; redirects are not followed
status_path, status_valueJSON pointer and expected JSON value, e.g. /ok and true
db_latency_pathPointer to a nonnegative millisecond number, e.g. /databaseMs
db_status_path, db_status_valuePointer and healthy value, e.g. /database, "connected"; an unhealthy DB makes the check fail
version_pathPointer to version string; empty disables extraction
brandingName, HTTPS logo URL, accent #RRGGBB, HTTPS footer links
templatesincident, recovery, maintenance; {name} expands for automatic incidents
subscribersenabled, public HTTPS URL, and sender
sender.kinddisabled, smtp or resend
SMTP fieldsfrom, smtp_host, smtp_port (587), smtp_username, smtp_password, smtp_starttls (true; false means implicit TLS, not plaintext)
HTTP fieldsfrom, http_url (Resend API endpoint), api_key; speaks the Resend JSON API, not arbitrary providers or SES HTTP
publishmode: disk or worker; output_path, or HTTPS worker_url and token
alertsOptional gotify_url + gotify_token, gotify_priority (0–10, default 5); telegram_token + telegram_chat_id; email (requires sender)
cache_ttl_secs300; range 30–3600; controls HTTP/CDN cache headers

JSON pointers support nested fields and array indices. Escape ~ as ~0 and / as ~1. Empty paths disable the corresponding extraction. A monitor with no JSON paths is a plain HTTP/body check. Response bodies must be at most 64 KiB. Status values in the admin form are JSON, so a string requires quotes.

Uptime percentages are confirmed successful check samples / all confirmed check samples, not continuous wall-clock SLA measurements. Retries collapse into one final sample. Days without samples are grey and excluded. Check intervals resume after a cycle completes; a confirmed failure takes up to the original timeout plus retry delays/timeouts. A current component becomes unknown after its check freshness threshold (two intervals plus retry budget). The public page independently warns at 15 minutes without a new snapshot. Raw checks are retained for 100 days; hourly chart data is aggregated on publication. Incident history is retained until you remove the database. All dates in payloads use UTC; the public page shows incident times in the visitor's timezone.

Environment variablePurpose
ADMIN_AUTHprivate (default): direct admin access through loopback/private IP, with same-origin checks
ADMIN_TOKENOptional 32+ character bearer credential for API automation; required only with ADMIN_AUTH=bearer
SETTINGS_KEYRequired 64 hexadecimal characters; encrypts settings/outbox/link tokens. Back it up; changing it makes old data unreadable
ADMIN_BINDNative default 127.0.0.1:8088; container listens internally on 0.0.0.0:8088
ALLOW_LAN_ADMINExplicit true needed for non-loopback binding; firewall/network isolation remains required
DATABASE_URLDefault sqlite:///data/status.sqlite
INITIAL_CONFIGSeed JSON path; container defaults /etc/rxkite-status/initial.json
WORKER_URL, WORKER_TOKENOverride saved publishing target/token; Worker URL selects worker mode
SMTP_PASSWORD, EMAIL_API_KEY, TELEGRAM_TOKEN, GOTIFY_TOKENOverride encrypted saved sender/alert secrets
STATUS_OUTPUTOverride local disk output path
BACKUP_DIRDefault /data/backups; SQLite backup destination
RUST_LOGLog filter, default rxkite_status=info
SSL_CERT_FILEOptional trusted PEM CA bundle for outbound HTTP; TLS verification stays enabled

Environment overrides take precedence at runtime. The admin leaves saved credential inputs empty with a “Saved credential — unchanged” placeholder. Enter a new value to rotate it or use Clear saved credential to remove it; the API represents preservation as __unchanged__. To remove an environment override, edit .env and recreate the container. Never put real keys in the example or commit .env/.dev.vars.

⁠Admin API

The private Rust service opens directly through a loopback/private IP address or .local hostname. Browser API requests must use the same origin; public Host headers and cross-origin requests are rejected. Compose publishes only 127.0.0.1:8088; keep the SSH tunnel for remote access. Optional ADMIN_AUTH=bearer protects API automation with Authorization: Bearer $ADMIN_TOKEN and does not enable the direct dashboard. GET /healthz is unauthenticated and checks SQLite liveness. This is distinct from the public Worker's /api/status and subscriber routes.

curl --fail-with-body http://127.0.0.1:8088/api/status

curl --fail-with-body http://127.0.0.1:8088/api/incidents \
  -H 'Content-Type: application/json' \
  -d '{"title":"Mumbai service disruption","message":"We are investigating.","kind":"incident","component_ids":["bom-1"]}'

# Replace INCIDENT_ID with the returned id. Each update is retained.
curl --fail-with-body http://127.0.0.1:8088/api/incidents/INCIDENT_ID/updates \
  -H 'Content-Type: application/json' \
  -d '{"state":"resolved","message":"Service has recovered."}'

# Maintenance times are Unix seconds; substitute your future window.
curl --fail-with-body http://127.0.0.1:8088/api/incidents \
  -H 'Content-Type: application/json' \
  -d '{"title":"Scheduled maintenance","message":"A short interruption is possible.","kind":"maintenance","component_ids":["public"],"starts_at":1900000000,"ends_at":1900003600}'
Method / pathOperation
GET, PUT /api/configRead redacted complete settings / validate and replace full settings
GET /api/statusGenerate public-shaped snapshot from SQLite
POST /api/checkTest one monitor once without saving a sample, creating incidents or changing its schedule
POST /api/publishPublish immediately; returns the published snapshot
GET /api/incidents?limit=100&offset=0Paginated full retained announcement history, maximum page 200
POST /api/incidentsCreate incident or planned maintenance
POST /api/incidents/:id/updatesAppend message + state; optionally edit title or both maintenance start/end values
GET /api/subscribersConfirmed/pending counts, queued/retrying delivery counts; no subscriber addresses
POST /api/backupConsistent SQLite VACUUM INTO backup; returns its path

States: investigating, identified, monitoring, resolved for incidents; scheduled, in_progress, resolved for maintenance. Resolved announcements are immutable; create a new one for a new event. Automatic incidents are owned by the monitor; recovery resolves them. Create a separate manual incident for broader operational context. Creation/update emails are queued transactionally, and publication runs at least once a minute. Scheduled windows affect the public banner while active; use the admin/API to report progress and completion.

⁠Subscriber flow and delivery

  1. Public page POSTs an email to the same-origin Worker. D1 rate limits 5 signups/IP/hour and 3/address/day using salted digests, and stores a pending event.
  2. Every minute the Pi pulls up to 50 events. A SQLite transaction records each event receipt, creates a pending subscriber and queues a confirmation email. The Pi acknowledges D1 only after the transaction commits. Retries are idempotent.
  3. The confirmation link opens a page; pressing its button queues a token digest. The Pi verifies its local hash and 48-hour expiry and confirms the subscriber. Email scanners opening the GET link cannot confirm it.
  4. Confirmed incidents, updates, resolutions and maintenance announcements enqueue mail only for confirmed subscribers. Each subscriber message includes an unsubscribe link.
  5. An unsubscribe action is queued even while signups are disabled. When the Pi processes it, it deletes the subscriber and cancels pending subscriber mail. Queue processing/email delivery waits while the Pi is offline. D1 expires unprocessed events after 7 days; link expiry may require subscribing again after a long outage.

Pending signup addresses and token digests in D1 are private behind the publish token. SQLite subscriber addresses are personal data; protect the volume, backups, and encryption key. Do not enable request-body/query logging in Worker observability: link URLs are bearer capabilities. Delivery is at least once after crashes; Resend receives an idempotency key and SMTP receives a stable Message-ID, but SMTP cannot guarantee deduplication. Delivery failures back off up to six hours and remain visible in admin counts. Set up/verify an email domain with SPF/DKIM/DMARC before enabling subscribers. Provider quotas and email delivery depend on your configured account.

⁠Free-tier design and resource bounds

R2 stores the snapshot because KV's free 1,000 writes/day would be exceeded by per-check publishing plus minute heartbeats. With five checks at 3/5-minute intervals plus heartbeats, approximately 3,648 writes/day (under 114,000/month) fit within R2's 1 million free monthly Class A operations; 5-minute private R2 checks add about 8,640 Class A writes and 8,640 Class B reads/month. Public-file probes only read an object. R2's free storage allowance is ample for one capped 1 MiB snapshot. The public frontend and private admin refresh every 30 seconds. Monitor intervals, retries, publishing/heartbeat cadence and cache TTL are independent and unchanged. The Worker caches status responses at the edge for 300 seconds by default and serves static assets through Workers Assets. D1 empty queue polling uses around 1,440 requests/day. No KV namespace or R2 access key is needed by the Pi: its Worker binding performs object access.

Cloudflare Workers Free has request/CPU limits and D1/R2 have usage limits. Stay on the Free Workers plan, monitor the Cloudflare dashboard, and set usage/billing alerts. R2 may require billing activation; usage beyond its free allowance can be charged. The architecture fits the expected small status-page workload; it does not guarantee unlimited traffic without cost. Large subscriber lists also depend on sender free quotas. The monitor limits concurrent checks to four and SQL connections to three, pages notification recipients, caps health bodies, and aggregates history instead of loading raw samples. The runtime RAM target is below 50 MB for the sample workload, not a hard guarantee for 50 monitors or a large outbox; Compose provides a 96 MB ceiling.

⁠Deployment and backups

See docs/deployment.md⁠ for Pi provisioning, exact Cloudflare commands, custom domain status.rxkite.in, sender configuration, smoke checks, multi-platform images, updates and consistent backup/restore. See docs/validation.md⁠ for what was actually tested.

Portainer Compose: https://github.com/dallergy/rxkite-status/blob/main/docker-compose.portainer.yml⁠ Step-by-step guide: https://github.com/dallergy/rxkite-status/blob/main/docs/deployment.md⁠

Tag summary

Content type

Image

Digest

sha256:eca489caa…

Size

31.2 MB

Last updated

12 minutes ago

docker pull shounak6942/rxkite-status