JSON API served by the same Dart process as the static Vue page. All routes, including static assets, send a 30-second cache policy intended for Cloudflare.
Every response includes:
Cache-Control: public, max-age=30, s-maxage=30, stale-while-revalidate=30
CDN-Cache-Control: max-age=30
Cloudflare-CDN-Cache-Control: max-age=30
Cloudflare does not cache extensionless JSON by default. Add a Cache Rule:
- If URI Path starts with
/(or at least/api/) - Then: Eligible for cache, Edge TTL 30 seconds, respect origin
Cache-Control
That keeps origin hits close to two per unique URL per minute (plus one per POP on refresh).
The first detail tab displays operator notices from
https://status.schulportal.hessen.de/. The backend fetches that stable URL at
startup and every five minutes, following up to five redirects. No versioned
WordPress URL is configured or stored. Redirects must use HTTPS on
schulportal.hessen.de or one of its subdomains, without credentials or custom
ports. A move outside this domain requires a deliberate code change.
Fetches have a 25-second overall deadline and a 2 MiB decoded-body limit. Only a complete parse of all six service sections replaces the SQLite snapshot; network errors or layout changes retain the last successful snapshot across restarts. API requests never trigger upstream requests. The frontend polls the cached API every minute, independently of login-monitoring requests.
The parser produces plain text and fixed status enums only. Scripts, styles,
embedded content and other active subtrees are discarded. No upstream URLs or
HTML attributes are forwarded. The frontend uses Vue text interpolation only,
and the original-page link is a fixed HTTPS URL. Encoded markup is displayed as
literal text. Responses also send nosniff and a CSP prohibiting inline scripts,
objects, frames embedding this page, and foreign script/connect sources.
Inline CSS remains allowed for the existing charts; upstream CSS is discarded.
test/fixtures/official.html records the official article on 2026-10-25 for
offline parser and browser tests. Fixture mode does not contact the official site.
A further 50 Wayback snapshots of the original and -3 pages (2023–2026) are
covered by offline regressions; see the historic parser audit
for scope, findings, source URLs and reproduction commands.
Last successfully parsed operator notices (independent of our login probes):
{
"sourceUrl": "https://status.schulportal.hessen.de/",
"fetchedAt": 1792926000,
"stale": false,
"services": [
{
"id": "PaedOrg",
"name": "Pädagogische Organisation (PaedOrg)",
"status": "restricted",
"maintenance": true,
"notice": "Operator status notice and previous restriction details",
"update": "Scheduled maintenance and previous update details"
}
]
}status is operational, restricted, or unknown, based on the operator's
summary symbols; it is never inferred from our probes. maintenance is separate
because maintenance and restrictions can coexist. fetchedAt is the retrieval
time, not the operator's publication time. stale is true after a failed fetch,
when the snapshot is at least ten minutes old, or when no snapshot exists.
Before the first successful fetch, fetchedAt is null and services is empty.
The endpoint returns 200 in this case so the frontend can show an unavailable
state without affecting the monitoring tabs.
Secondary check: current online status only.
{
"online": true,
"status": "operational",
"checkedAt": 1784116800,
"responseMs": 240,
"code": 0,
"inHolidays": true,
"holidayLabel": "Sommerferien"
}status is one of operational, down, misconfigured.
online is true only when status is operational.
code is the last probe result:
| code | meaning |
|---|---|
| 0 | ok |
| 1 | timeout |
| 2 | HTTP / network / Lanis down |
| 3 | authentication (monitor credentials) |
| 4 | unexpected error |
Persistent code=3 is treated as misconfigured, not as an SPH outage.
Current status plus uptime for fixed windows.
{
"current": { "online": true, "status": "operational" },
"windows": {
"24h": { "uptime": 97.917, "avgMs": 250, "checks": 1441, "failures": 30 },
"7d": { "uptime": 99.053, "avgMs": 260, "checks": 3169, "failures": 30 },
"30d": { "uptime": 99.2, "avgMs": 255, "checks": 800, "failures": 4 },
"90d": { "uptime": 99.4, "avgMs": 252, "checks": 1200, "failures": 6 },
"180d": { "uptime": 99.6, "avgMs": 250, "checks": 2000, "failures": 7 },
"1y": { "uptime": 99.7, "avgMs": 249, "checks": 3000, "failures": 8 },
"2y": { "uptime": 99.8, "avgMs": 248, "checks": 4000, "failures": 8 }
}
}Uptime is 100 * (checks - failures) / checks over raw samples in that window.
Measured failure spans for the last 30 days, derived from individual probe results (not chart buckets). Each span runs from the first failed check until the next successful check. Isolated single-probe failures are omitted; a span is listed only when at least two consecutive checks failed.
{
"from": 1781524800,
"to": 1784116800,
"incidents": [
{ "start": 1784111400, "end": 1784113200 },
{ "start": 1782900000, "end": 1782910800 }
]
}Fixed paths so Cloudflare cache keys stay stable. Unknown windows return 404.
| window | bucket |
|---|---|
24h |
1 minute |
7d |
5 minutes |
30d |
1 hour |
90d |
2 hours |
180d |
4 hours |
1y |
6 hours |
2y |
6 hours |
{
"window": "24h",
"bucketSeconds": 60,
"from": 1784030400,
"to": 1784116800,
"points": [[1784030400, 220, 1], [1784030460, null, 0]]
}Each point is [ts, ms|null, ok|null]. ok is 0 if any sample in the bucket failed, 1 if all succeeded, or null if the bucket has no samples. ms is the average of successful samples, or null when unavailable.
Hesse Schulferien covering the two-year history window. Served only from the local SQLite snapshot. The origin fetches https://schulferien-api.de/api/v1/{year}/HE at most once every 7 days. Client requests never reach that API.
{
"state": "HE",
"updatedAt": 1784116800,
"periods": [
{
"start": 1782691200,
"end": 1786147140,
"name": "sommerferien",
"label": "Sommerferien"
}
]
}| variable | default | notes |
|---|---|---|
LANIS_SCHOOL_ID |
— | required in live mode |
LANIS_USERNAME |
— | required in live mode |
LANIS_PASSWORD |
— | required in live mode |
PORT |
8080 |
listen port |
DATABASE_PATH |
data/status.db |
use :memory: in fixture mode |
STATUSMONITOR_MODE |
live |
fixture skips probes and holiday refresh |
STATUSMONITOR_NOW |
wall clock | pinned unix seconds for fixtures |
STATUSMONITOR_STATIC_DIR |
frontend/dist |
Vue build output |
STATUSMONITOR_FIXTURE_DIR |
test/fixtures |
JSON seed files |
liblanis is GPL-3.0. A distributed binary that links it should be treated as GPL-3.0 even though this repository’s LICENSE file is MIT.