Poll status.liu.se for the live campus pulse #61

Open
opened 2026-10-19 21:41:34 +00:00 by sermuns · 1 comment
Owner

Linköping University publishes a live, public occupancy proxy at
https://status.liu.se/: Närvaro på campus, the number of people attached to
the campus Wi-Fi, per building, refreshed roughly every ten minutes.

The page is not an API, but its own scripts consume one:

  • /dynamic/presence/linköping.csv — the Linköping total, three columns after
    the timestamp: students, employees, guests.
  • /dynamic/presence/liu.csv — all of LiU, same shape.
  • /dynamic/presence/<building>.csv — one building, same shape. Observed ids
    include ahuset, bhuset, chuset, dhuset, ehuset, fysikhuset,
    studenthuset, kårallen, terra, temahuset, ihuset, zenit and
    vallfarten, plus the Norrköping and Lidingö equivalents.
  • /dynamic/presence.html — the rounded top-list the page renders.

The CSV is a ~14-day rolling window at ten-minute resolution, no header,
ISO-like timestamps without a zone. Values are rounded to the nearest ten, so
a building showing 10 at 03:00 is noise, and the per-building rows do not add
up to the city total. The response carries Last-Modified, ETag and
Accept-Ranges: bytes; Range: bytes=-512 returns the tail, which is all a live
update needs. There is no key, no account and no CORS header.

Why the server and not the bake or each client:

  • the data is live, so baking it is wrong;
  • the browser cannot read it directly without CORS, and native has no HTTP path
    of its own today;
  • it is shared truth in the same sense as day/night, and the server is already
    the one thing that owns shared truth;
  • one fetch per client would multiply the requests for no benefit.

Proposed shape:

  • liuverse-server::presence: a task that fetches the tail of linköping.csv
    and each mapped building CSV every ten minutes, with If-None-Match /
    If-Modified-Since and a backoff on failure. At startup, fetch the full CSVs
    once to learn a per-building peak for normalisation.
  • Append ServerMsg::CampusPresence { total: Presence, buildings: Vec<BuildingPresence>, age_seconds: u16, stale: bool }; Presence { students, employees, guests }. BuildingPresence { building: u16, presence: Presence, peak: u32 }. building is the index into the embedded
    BakedWorld::buildings, resolved by name once at startup, so both ends agree
    without shipping strings every tick.
  • Send the last snapshot to every new client after Welcome, broadcast on
    change. Keep the last good snapshot across fetch failures and mark it stale.
  • Map only Campus Valla buildings. ihuset covers more than one baked shell and
    needs a decision; keyhuset has no series and is excluded.
  • A malformed row is skipped, never fatal. A missing building is skipped, never
    fatal. No custom User-Agent.
  • reqwest is already a workspace dependency (the bake uses its blocking half);
    the server has no HTTP client yet. Add it there, not a new HTTP crate.
  • docs/status.md with the endpoints, cadence, rounding, coverage and the
    slug-to-building table, linked from ARCHITECTURE.md's sources.

This is an occupancy proxy, not a headcount: it is Wi-Fi devices, rounded, and
it includes staff and guests. Say so wherever it is shown.


Related

  • #47 the same server-authoritative external data
  • #62 where it is shown
  • #63 and #65 what it answers
  • #64 the night it lights
Linköping University publishes a live, public occupancy proxy at `https://status.liu.se/`: **Närvaro på campus**, the number of people attached to the campus Wi-Fi, per building, refreshed roughly every ten minutes. The page is not an API, but its own scripts consume one: - `/dynamic/presence/linköping.csv` — the Linköping total, three columns after the timestamp: students, employees, guests. - `/dynamic/presence/liu.csv` — all of LiU, same shape. - `/dynamic/presence/<building>.csv` — one building, same shape. Observed ids include `ahuset`, `bhuset`, `chuset`, `dhuset`, `ehuset`, `fysikhuset`, `studenthuset`, `kårallen`, `terra`, `temahuset`, `ihuset`, `zenit` and `vallfarten`, plus the Norrköping and Lidingö equivalents. - `/dynamic/presence.html` — the rounded top-list the page renders. The CSV is a ~14-day rolling window at ten-minute resolution, no header, ISO-like timestamps without a zone. Values are **rounded to the nearest ten**, so a building showing `10` at 03:00 is noise, and the per-building rows do not add up to the city total. The response carries `Last-Modified`, `ETag` and `Accept-Ranges: bytes`; `Range: bytes=-512` returns the tail, which is all a live update needs. There is no key, no account and no CORS header. Why the server and not the bake or each client: - the data is live, so baking it is wrong; - the browser cannot read it directly without CORS, and native has no HTTP path of its own today; - it is shared truth in the same sense as day/night, and the server is already the one thing that owns shared truth; - one fetch per client would multiply the requests for no benefit. Proposed shape: - `liuverse-server::presence`: a task that fetches the tail of `linköping.csv` and each mapped building CSV every ten minutes, with `If-None-Match` / `If-Modified-Since` and a backoff on failure. At startup, fetch the full CSVs once to learn a per-building peak for normalisation. - Append `ServerMsg::CampusPresence { total: Presence, buildings: Vec<BuildingPresence>, age_seconds: u16, stale: bool }`; `Presence { students, employees, guests }`. `BuildingPresence { building: u16, presence: Presence, peak: u32 }`. `building` is the index into the embedded `BakedWorld::buildings`, resolved by name once at startup, so both ends agree without shipping strings every tick. - Send the last snapshot to every new client after `Welcome`, broadcast on change. Keep the last good snapshot across fetch failures and mark it stale. - Map only Campus Valla buildings. `ihuset` covers more than one baked shell and needs a decision; `keyhuset` has no series and is excluded. - A malformed row is skipped, never fatal. A missing building is skipped, never fatal. No custom User-Agent. - `reqwest` is already a workspace dependency (the bake uses its blocking half); the server has no HTTP client yet. Add it there, not a new HTTP crate. - `docs/status.md` with the endpoints, cadence, rounding, coverage and the slug-to-building table, linked from `ARCHITECTURE.md`'s sources. This is an occupancy *proxy*, not a headcount: it is Wi-Fi devices, rounded, and it includes staff and guests. Say so wherever it is shown. --- **Related** - #47 the same server-authoritative external data - #62 where it is shown - #63 and #65 what it answers - #64 the night it lights
sermuns added this to the Living campus milestone 2026-10-19 22:18:50 +00:00
Author
Owner

The data is interesting, but this is the riskiest issue in the batch and it has four follow-ups hanging off it (#62–#65). It depends on a page that is explicitly not an API, has no CORS, is rounded to ten and ten minutes stale, and can disappear without notice. That is fine for an ambient "campus is busy" signal, not for a protocol-level feature stack. I'd land #61 alone behind a timeout and an unknown fallback, and explicitly make #62–#65 optional. Also be careful with the framing: "the building has 340 people" is a stronger claim than the source supports.

The data is interesting, but this is the riskiest issue in the batch and it has four follow-ups hanging off it (#62–#65). It depends on a page that is explicitly not an API, has no CORS, is rounded to ten and ten minutes stale, and can disappear without notice. That is fine for an ambient "campus is busy" signal, not for a protocol-level feature stack. I'd land #61 alone behind a timeout and an `unknown` fallback, and explicitly make #62–#65 optional. Also be careful with the framing: "the building has 340 people" is a stronger claim than the source supports.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
elsie/liuverse#61
No description provided.