Rewrite README and correct CLAUDE.md to match current architecture
95bccf0f
by Isaac Bythewood
· 23 days ago
Rewrite README and correct CLAUDE.md to match current architecture
- README: replace analytics-copied intro with a real uptime service
description, correct feature list, switch yarn → bun, drop dead
ipinfo/location section
- CLAUDE.md: fix mixin location (models.py, not mixins/), swap Scrapy
subprocess for the in-process requests + BeautifulSoup crawler,
expand scheduler / chromium wrapper / production notes
- Changelog: add 2026-04-18 entry and round out 2026-04-17 with
scheduler refactor, supervisor collapse, direct-to-MX email, and
PYTHONUNBUFFERED
@@ -33,24 +33,24 @@ Black profile: 88 char lines. isort uses `profile=black`. Flake8 ignores E203. A### Django Apps- **accounts** - Custom User model (UUID PK) with `discord_webhook_url` field. Standard auth views.- **properties** - Core app. `Property` model tracks a URL's monitoring state; `Check` model stores individual HTTP check results. Property behavior is split into mixins: `SecurityMixin` (header analysis), `AlertsMixin` (state machine for up/down notifications), `CrawlerMixin` (Scrapy SEO data).- **properties** - Core app. `Property` model tracks a URL's monitoring state; `Check` model stores individual HTTP check results. Property behavior is split into mixin classes defined alongside the models in `properties/models.py`: `SecurityMixin` (header analysis), `AlertsMixin` (state machine for up/down notifications), `CrawlerMixin` (SEO data from the in-process crawler).- **pages** - Static marketing pages (home, changelog, robots.txt, sitemap).### Scheduler (`properties/management/commands/scheduler.py`)Single management command that runs in an infinite loop (30-second cycle). Uses two thread pools with `queue.Queue`:Single management command that runs in an infinite loop (30-second cycle) using two thread pools with graceful shutdown:- **Status checks**: Properties where `next_run_at` has passed (3-min intervals). HTTP timeout is 10s. SSL errors map to status 526, timeouts to 408.- **Lighthouse + Crawler**: Daily Lighthouse via Node CLI wrapper (`status/lighthouse.py`), weekly Scrapy crawl via subprocess (`crawler/runner.py`).- **Lighthouse + Crawler**: Daily Lighthouse via Node CLI wrapper (`status/lighthouse.py`), weekly SEO crawl via the in-process crawler (`crawler/runner.py` — `requests` + `BeautifulSoup`, no subprocess).Cleans up checks older than 3 days each cycle.### Alert State Machine (`properties/mixins/alerts.py`)### Alert State Machine (`AlertsMixin` in `properties/models.py`)Two states: `up` (default) and `down`. Transitions:- **UP -> DOWN**: Requires 2 consecutive non-200 checks (avoids false positives).- **DOWN -> UP**: Immediate on 200 status code.Alerts (email + Discord webhook) only fire on state transitions, not on every check.Alerts (email + Discord webhook) only fire on state transitions, not on every check. State is committed before notifications fire so a crash mid-alert cannot cause duplicate sends.### Frontend (Vite)
@@ -63,14 +63,14 @@ Three entry points in `vite.config.js`, all output to `status/static/` with stab### External Tool Wrappers- `status/lighthouse.py` - Invokes `node_modules/.bin/lighthouse` with headless Chrome flags, parses JSON scores.- `status/chromium.py` - Headless Chromium for PDF report generation. Auto-detects `chromium` vs `chromium-browser` binary.- `crawler/` - Scrapy project with `seo_spider.py` CrawlSpider. Extracts title, meta description, canonical URL, OG tags, H1 per page.- `status/lighthouse.py` - Invokes `node_modules/.bin/lighthouse` with headless Chrome flags, parses JSON scores. Surfaces failure reasons in the UI.- `status/chromium.py` - Headless Chromium for PDF report generation. Auto-detects `chromium`, `chromium-browser`, or `google-chrome` binary. Hardened with timeouts, `/tmp` temp files, and `--headless=new`.- `crawler/` - In-process SEO crawler using `requests` + `BeautifulSoup` (no Scrapy). Split across `fetcher.py`, `parser.py`, `checks.py`, and `runner.py`. Extracts title, meta description, canonical URL, OG tags, and H1 per page.### SettingsSplit settings: `status/settings/__init__.py` (shared), `development.py`, `production.py`. Django picks based on `DJANGO_SETTINGS_MODULE` env var (defaults to development). Production uses env vars for SECRET_KEY, BASE_URL, IPINFO_TOKEN and stores SQLite at `/data/db/`.Split settings: `status/settings/__init__.py` (shared), `development.py`, `production.py`. Django picks based on `DJANGO_SETTINGS_MODULE` env var (defaults to development). Production uses env vars for SECRET_KEY and BASE_URL, and stores SQLite at `/data/db/`. SQLite settings (WAL, PRAGMA tuning) are centralized in shared settings.### ProductionDocker Compose runs a single `web` service. `entrypoint.py` spawns Gunicorn (Uvicorn workers) and the `scheduler` management command side-by-side in that container — if either process exits, the container stops and Docker restarts it. Deploys via `git push server master` triggering a post-receive hook.Docker Compose runs a single `web` service (Alpine 3.21 base). `entrypoint.py` spawns Gunicorn (Uvicorn workers) and the `scheduler` management command side-by-side in that container — if either process exits, the container stops and Docker restarts it. `PYTHONUNBUFFERED=1` so scheduler output reaches `docker logs`. Deploys via `git push server master` triggering a post-receive hook.
@@ -1,24 +1,28 @@# StatusA self-hostable status service with a straightforward API to collect eventsfrom any source.A self-hostable uptime monitor and status page. HTTP checks every 3 minutes,daily Lighthouse audits, weekly in-process SEO crawls, and alerts via emailand Discord webhook on state transitions.## MotivationI was bored and felt like writing my own status service over the weekend.I was bored and felt like writing my own uptime service over the weekend.## Features- Standard website status collection- Custom metrics collection- UTM query collection- Optional anonymized location collection- Customizable UI- Date range selection and comparison- Public URL sharing- Customizable- HTTP uptime checks with rolling uptime percentages and recent-uptime bars- Lighthouse audits (performance, accessibility, best practices, SEO) with weighted breakdown and top savings opportunities- In-process SEO crawler (requests + BeautifulSoup) — title, description, canonical, OG tags, and H1 per page- Security header analysis (HSTS, CSP, X-Frame-Options, Referrer-Policy, etc.)- Alert state machine with debounce on flaps — two consecutive non-200s to go down, immediate 200 to come back up- Email and Discord webhook alerts on state transitions only- PDF report export per property via a headless Chromium subprocess- Customizable UI with a warm-earth palette and Monaspace Argon## Requirements
@@ -32,9 +36,10 @@ dependencies:- python- uv- node- yarn- chromium- bun- node (required only for the `lighthouse` npm CLI — Bun doesn't run Lighthouse correctly; see bun issue #4958)- chromium (used for PDF report generation via a subprocess wrapper)You can also check the `Dockerfile` for an exact list of dependencies and adjustpackage names for your desired platform.
@@ -58,10 +63,10 @@ If you want to also run the scheduler you'll have to do so separately. Run## Checking outdated dependenciesThis can be done in both yarn and uv with the following two commands:This can be done in both bun and uv with the following two commands: uv lock --upgrade --dry-run yarn outdated bun outdatedYou can then upgrade all dependencies at once with:
@@ -92,31 +97,16 @@ variables.## Default userThe default user is `admin` with the password `admin`. We also create an exampleproperty so you can see how the status look and a property to collect metricsfrom ourselves.The default user is `admin` with the password `admin`. Add your own propertiesfrom the dashboard after signing in.## User location data## AlertsI'm unsure how I want to handle user location data at the moment. I'm not reallyinterested in someone's personal location but I do like to know where peopleare coming from region wise. This helps me know if I need to add translationsto my projects or if I need to add maybe a CDN/caching/server to a new region.For that reason I've added a simple way to enable or disable location data. Idon't want to store user IPs so location data isn't retroactive but if you adda new environmental variable for an [ipinfo.io](https://ipinfo.io/) API tokenwe start logging location data based on IP then throw out the IP and only keepthe data.This platform could easily be replaced with any platform you like, if you searchthe codebase for "ipinfo" you can find and replace as needed. Also, if you stripout all location collecting information this project still runs just fine.What to add to your `.env` file: IPINFO_TOKEN=<your ipinfo.io token>Each property can be assigned a Discord webhook URL per user (see the accountsettings). Email alerts use the project's configured outbound mailer (thedirect-to-MX backend by default, see `status/mailer.py`). Alerts fire on statetransitions only — not on every failing check.## Backups
modified
pages/templates/pages/changelog.html
@@ -35,6 +35,17 @@ <div class="row mt-4"> <div class="col-lg-8 offset-lg-2"> <div class="changelog-entry"> <div class="changelog-date">2026-04-18</div> <ul> <li>Centralize SQLite settings and expand PRAGMA tuning (WAL, synchronous, cache, mmap, temp_store)</li> <li>Commit alert state before firing notifications so a crash mid-alert can't cause duplicate sends</li> <li>Harden the chromium wrapper with timeouts, <code>/tmp</code> temp files, and <code>--headless=new</code></li> <li>Redesign alert emails to match the warm-earth site palette</li> <li>Stop wedge-reset from failing a healthy queued backlog of checks</li> </ul> </div> <div class="changelog-entry"> <div class="changelog-date">2026-04-17</div> <ul>
@@ -45,6 +56,10 @@ <li>Swap the 🚀 favicon for a themed SVG tile matching the in-app logo</li> <li>Update the chromium wrapper to also pick up <code>google-chrome</code> so the webdev container runs locally without extra setup</li> <li>Fix logout 405 by switching to a POST form + CSRF (Django 5 <code>LogoutView</code> is POST-only)</li> <li>Refactor the scheduler with thread pools and graceful shutdown</li> <li>Collapse the web and scheduler services into a single container via a Python supervisor in <code>entrypoint.py</code></li> <li>Replace the Exim relay with a Python direct-to-MX email backend</li> <li>Set <code>PYTHONUNBUFFERED</code> so scheduler output reaches <code>docker logs</code></li> </ul> </div>