Files
npm-statuspage/README.md
Fredrik Johansson b83d7aec01 Add admin UI: per-host visibility overrides and manual targets
NPM stays the source of truth for which domains exist; this adds an
optional enrichment layer on top of the existing NPM_INCLUDE_PATTERNS
filtering, gated behind ADMIN_PASSWORD (disabled entirely if unset).

- Force-show/force-hide any NPM-discovered host regardless of what
  the include pattern would otherwise decide
- Per-host display name and check path overrides
- Manually add targets that aren't proxied through NPM at all
- Every override falls back to the NPM-derived default when unset —
  a host with no override behaves exactly as before this existed

Host overrides and manual targets live in SQLite (better-sqlite3,
WAL mode) and are re-read on every 60s check tick, so admin changes
take effect within one cycle with no restart. NPM host discovery
keeps its original 10-minute cadence — only the local override reads
happen every tick.

The core merge (NPM hosts + overrides + manual targets -> final
check list) is a pure function in targets.ts, kept separate from the
checker's I/O for testability. Verified end-to-end against a local
mock NPM server (auth, force-show/hide, rename, custom check path,
manual targets, reset-to-default all propagate correctly) and against
the actual Docker build/runtime (better-sqlite3's native addon builds
and loads correctly in the Alpine image).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 15:40:57 +02:00

4.6 KiB

status

A lightweight HTTP status checker that sources its target list from Nginx Proxy Manager and serves results as JSON and a terminal-aesthetic HTML page.

Designed to run as a standalone public status page on its own subdomain — if your main site is down, the status page is still reachable independently.

How it works

  1. On startup (and every 10 minutes), fetches all enabled proxy hosts from the NPM API
  2. Filters to domains matching NPM_INCLUDE_PATTERNS — unless overridden per-host in the admin UI (see below)
  3. HTTP-checks each host every 60 seconds
  4. Serves current results at / (HTML) and /api/status (JSON)

The HTML page auto-refreshes every 60 seconds.

Admin UI

NPM stays the source of truth for which domains exist. The optional admin UI at /admin lets you override how they're presented, without touching NPM itself:

  • Force-show or force-hide any NPM-discovered host, regardless of what NPM_INCLUDE_PATTERNS would otherwise decide
  • Rename a host's display name (e.g. "Wisp" instead of wisp.dev.xplwd.com)
  • Custom check path per host, for anything whose root path isn't a meaningful health check
  • Manually add non-NPM targets — anything not proxied through NPM (a raw-port service, or a third-party dependency you want on the same page), checked exactly like a discovered host

Every override is enrichment, not replacement: leave a field blank and it falls back to the NPM-derived default (domain name, root path, pattern-match result). A host removed from NPM entirely just stops appearing — its override row, if any, becomes inert rather than erroring.

Overrides are stored in SQLite (DATA_DIR/statuspage.db) and re-read on every 60-second check tick, so changes in the admin UI take effect within one cycle — no restart needed. The 10-minute NPM host-discovery refresh is unaffected; only the local override/manual-target reads happen every tick.

Set ADMIN_PASSWORD to enable /admin and its API (/api/admin/*, gated by an X-Admin-Password header, same stateless pattern as wisp's upload gate). Leave it unset and /admin is disabled outright — the main status page and /api/status work exactly as before, unaffected.

Running locally

cp .env.example .env
# fill in NPM_EMAIL and NPM_SECRET
npm install
npm run dev

Environment variables

Variable Default Description
PORT 3035 Port to listen on
NPM_BASE_URL http://localhost:81 Base URL of your Nginx Proxy Manager instance
NPM_EMAIL NPM account email
NPM_SECRET NPM account password
NPM_INCLUDE_PATTERNS \.example\.com$ Comma-separated regex patterns; only domains matching at least one are checked by default
ADMIN_PASSWORD Gates /admin. Unset disables the admin UI entirely
DATA_DIR ./data Where the SQLite DB (host overrides, manual targets) lives

Docker

docker compose up -d

Uses the image from IMAGE in .env. For a local build:

docker build -t status:local .
IMAGE=status:local docker compose up -d

CI/CD

Gitea Actions workflow at .gitea/workflows/build.yml. Builds and pushes to the Gitea container registry on every push to main. Requires a TKNTKN secret (PAT with packages:write) in the repo settings.

API

GET /api/status
{
  "lastRefreshed": "2026-07-07T19:35:43Z",
  "hosts": [
    {
      "name": "example.com",
      "url": "https://example.com",
      "status": "up",
      "statusCode": 200,
      "latencyMs": 182,
      "checkedAt": "2026-07-07T19:35:43Z"
    }
  ]
}

status is "up" for any response (including 4xx — the server is reachable), "down" for connection errors and 5xx, "unknown" before the first check completes.

Admin API

All routes below /api/admin (except /auth/check) require an X-Admin-Password header matching ADMIN_PASSWORD. Returns 503 for every route if ADMIN_PASSWORD is unset.

POST   /api/admin/auth/check              -- { ok: boolean }
GET    /api/admin/hosts                   -- every enabled NPM host, annotated with matchesPattern + any override
PUT    /api/admin/hosts/:domain           -- { visibility?, displayName?, checkPath?, sortOrder? }
DELETE /api/admin/hosts/:domain           -- reset to default (delete the override row)
GET    /api/admin/manual-targets
POST   /api/admin/manual-targets          -- { name, url, checkPath? }
PUT    /api/admin/manual-targets/:id      -- { name?, url?, checkPath?, enabled?, sortOrder? }
DELETE /api/admin/manual-targets/:id

visibility is one of "default" (follow NPM_INCLUDE_PATTERNS), "hidden", or "shown".