@@ -2,6 +2,7 @@/dist//data//node_modules//frontend/dist//frontend/node_modules/.envdb.sqlite3
@@ -25,6 +25,22 @@ from the Django era can be migrated in via `./status migrate <django.sqlite3>` embedded Typst, no chromium subprocess; markdown rendered from a template)## Stack| Concern | Crate / Tool ||-----------------|-------------------------------------------------------|| Web framework | axum + tokio || Database | sqlx + SQLite (WAL, `synchronous=NORMAL`) || Auth | tower-cookies signed sessions || Template engine | minijinja || HTTP client | reqwest (rustls) || Crawler | scraper + html5ever + robotstxt + hickory-resolver || Lighthouse | `bun run --bun node_modules/.bin/lighthouse` || Email | lettre (direct-to-MX, opportunistic STARTTLS) || PDF | embedded Typst (`typst` + `typst-pdf` + `typst-kit`) || Static assets | Vite + Bun, Bootstrap 5, Chart.js, monaspace font |## RequirementsYou need docker installed for a quick production start, or you can read the
@@ -33,9 +49,10 @@ You need docker installed for a quick production start, or you can read theFor local development:- rust (cargo) for the backend- bun for the frontend bundler (Vite)- node + npm only for the `lighthouse` npm CLI; bun doesn't run Lighthouse correctly (bun issue #4958)- bun for everything JS: the frontend bundler (Vite) AND the `lighthouse` CLI. The rust binary invokes lighthouse via `bun run --bun node_modules/.bin/lighthouse`, which symlinks `node` → `bun` so the shim's `#!/usr/bin/env node` shebang resolves to bun's runtime. No nodejs/npm required.- chromium for Lighthouse's own audits (PDF reports do not need chromium; they go through embedded Typst)
@@ -50,6 +67,38 @@ Server boots, applies migrations, and starts the scheduler in-process. OpenURL.## ConfigurationAll config comes from `.env` (loaded via `dotenvy`):| Variable | Required | Purpose ||---|---|---|| `STATUS_PASSWORD` | yes | Single operator password || `BASE_URL` | yes for prod | Used in absolute URLs (sitemap, og tags, alert email links). No trailing slash || `PORT` | no (default `8000`) | HTTP listen port || `STATUS_COOKIE_SECRET` | no | 32+ bytes for signing the session cookie. Falls back to a SHA-512 of the password, so rotating the password invalidates sessions || `ALERT_EMAIL` | no | Recipient for outage / recovery emails. Leave unset to disable email || `DISCORD_WEBHOOK_URL` | no | Discord webhook for outage / recovery embeds. Leave unset to disable || `STATUS_DATA_DIR` | no (default `./data`) | Where the SQLite db lives. Production sets this to `/data` || `STATUS_ROOT` | no | Override the project root (where `templates/`, `dist/`, `migrations/` are read from) || `CHROMIUM_BIN` | no | Path to chromium for Lighthouse. Falls back to `PATH` lookup, then a `/opt/playwright-browsers/` glob |## Make targets| Target | What it does ||---|---|| `make run` (default) | Vite watch + `cargo run` on port 8000, plus the in-process scheduler || `make build` | Vite assets + release binary (`target/release/status`) || `make start` | Run the release binary (after `make build`) || `make pull` | rsync the production sqlite db from `git remote server` into `data/` || `make migrate FROM=<path-to-django.sqlite3>` | One-shot import of an existing Django status database, preserving Property UUIDs so public status URLs keep working. Add `FORCE=1` to wipe first || `make push` | `git push` to every configured remote || `make clean` | Remove `target/`, `dist/`, `frontend/node_modules/`, root `node_modules/`, and `data/` |There are no tests or linters configured.## Importing an existing Django status DBIf you have a SQLite from the Django version of this project, you can keep
modified
frontend/bun.lock
@@ -6,12 +6,12 @@ "dependencies": { "@fontsource/monaspace-argon": "^5.2.5", "@popperjs/core": "^2.11.5", "bootstrap": "^5.1.3", "chart.js": "^3.7.1", "bootstrap": "^5.3.3", "chart.js": "^4.5.1", "js-cookie": "^3.0.1", }, "devDependencies": { "sass": "^1.53.0", "sass": "^1.97.3", "vite": "^6.3.1", }, },
@@ -71,6 +71,8 @@ "@fontsource/monaspace-argon": ["@fontsource/monaspace-argon@5.2.5", "", {}, "sha512-EJ+jq1Smm3BB+8RK/gwB1uzjrSKdycZkKm8OZGCYvqJiTChKcGe7b2lmj0PmQbb/+lAEdCBIAcFLlPaWhtRANA=="], "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="],
@@ -155,7 +157,7 @@ "bootstrap": ["bootstrap@5.3.8", "", { "peerDependencies": { "@popperjs/core": "^2.11.8" } }, "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg=="], "chart.js": ["chart.js@3.9.1", "", {}, "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w=="], "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
modified
frontend/package.json
@@ -8,12 +8,12 @@ "dependencies": { "@fontsource/monaspace-argon": "^5.2.5", "@popperjs/core": "^2.11.5", "bootstrap": "^5.1.3", "chart.js": "^3.7.1", "bootstrap": "^5.3.3", "chart.js": "^4.5.1", "js-cookie": "^3.0.1" }, "devDependencies": { "sass": "^1.53.0", "sass": "^1.97.3", "vite": "^6.3.1" }}
modified
frontend/static_src/properties/scripts/property_graphs.js
@@ -103,11 +103,13 @@ document.addEventListener("DOMContentLoaded", function () { }, scales: { x: { grid: { color: accent.grid, drawBorder: false }, grid: { color: accent.grid }, border: { display: false }, ticks: { autoSkip: true, maxRotation: 25, font: tickFont, color: accent.ticks }, }, y: { grid: { color: accent.grid, drawBorder: false }, grid: { color: accent.grid }, border: { display: false }, ticks: { beginAtZero: true, font: tickFont,
@@ -149,7 +151,7 @@ function buildDoughnut(canvasId, dataId) { new Chart(ctx, { type: "doughnut", data: { labels: data.map((d) => d.label), labels: data.map((d) => String(d.label)), datasets: [ { data: data.map((d) => d.count),
modified
templates/properties/property_report.md
@@ -6,7 +6,7 @@- Current status: **{{ property.current_status }}**- Average response: **{{ property.avg_response_time }} ms**- Recent uptime: {% if property.recent_uptime_pct is not none %}**{{ property.recent_uptime_pct }}%**{% else %}—{% endif %}- Recent uptime: {% if property.recent_uptime_pct is not none %}**{{ property.recent_uptime_pct }}%**{% else %}n/a{% endif %}- Total checks logged: **{{ property.total_checks }}**## Lighthouse
@@ -32,7 +32,7 @@ _No lighthouse data._## SEO insights{% if property.crawler_insights and property.crawler_insights | length > 0 %}{% for i in property.crawler_insights %}- [{{ i.severity }}] [{{ i.type }}] {{ i.issue }} — `{{ i.url }}`- [{{ i.severity }}] [{{ i.type }}] {{ i.issue }}: `{{ i.url }}`{% endfor %}{% else %}_No crawl results yet._