faraapacalda.ro answers one question for every street, heating point and sector in Bucharest: how many days a year does this place go without hot water? (RO: cate zile pe an sta Bucurestiul fara apa calda.)
The city's heating utility publishes current outages and deletes them once resolved. No
official archive exists. termo-data snapshots
the public pages every 15 minutes and publishes a nightly data-latest bundle; this repo
turns that bundle into a fully static site of ~2,350 pages.
Live: https://faraapacalda.ro
- Next 16 App Router, TypeScript strict, Tailwind v4, pure SSG. Every page sets
dynamic = 'error'and usesgenerateStaticParams. Data is read at build time throughlib/data.ts, so a missing bundle field fails the build instead of renderingNaN. - Entity pages:
/strada/[slug](street),/punct-termic/[slug](heating point),/sector/[id], plus/clasamentrankings,/hartamap,/cautasearch, and/metodologiewith a Dataset JSON-LD that carries the bundle timestamp. - Block finder: a street page resolves a block number to the heating point serving it, with the current outage status per sector.
- Feedback + app-demand poll: two tiny API routes writing to Supabase, the only non-static surface.
- AI-readable:
/llms.txtand per-page OG images.
The big number is days: calendar days with a hot-water stoppage (oprire). The
utility also publishes deficienta (pressure or temperature below spec); that is counted
separately as days_deficienta and shown site-wide, but never summed with days. The
distinction is what makes the number defensible.
node scripts/fetch-data.mjs # downloads + extracts the published bundle into .data/
npm test # vitest, 122 tests on fixtures in test/fixtures/data
npm run build
npx playwright test # e2e, needs .data/ populated and a production buildDATA_BUNDLE_PATH=/abs/path/bundle.tar.gz node scripts/fetch-data.mjs builds against a
local bundle, which is how site code gets developed against data the nightly has not
published yet.
- Deploy order. Pushing
maindeploys against the current bundle. Code that reads a newly added field renders as if it were absent until the next nightly triggers a second build, so reads of new fields degrade to a hidden section, not a broken page. The deploy signal is thedateModifiedon/metodologie, which equals the bundle'sgenerated_atto the minute. - Scraped names are untrusted input. Street and heating-point names come from the
utility's announcements and reach JSON-LD via breadcrumbs. Every JSON-LD payload goes
through one helper that escapes
<beforedangerouslySetInnerHTML; without it a name containing</script>would close the tag early. - A stray server on :3000 makes Playwright test stale code (
reuseExistingServer). A suite that finishes suspiciously fast is the tell.
Data reproduces public information published by CMTEB / Termoenergetica SA on cmteb.ro. Sector boundaries derive from OpenStreetMap ((c) OpenStreetMap contributors, ODbL). The git-scraping model follows FlorinPopaCodes/termoficare-data and gov2-ro/prometeu.
Code: MIT.