Skip to content

live status: show the estimated restore time where people actually land - #14

Merged
tiXor-code merged 2 commits into
mainfrom
feat/ongoing-restore-time
Sep 7, 2026
Merged

tiXor-code merged 2 commits into
mainfrom
feat/ongoing-restore-time

Conversation

@tiXor-code

Copy link
Copy Markdown
Owner

Why

Six of the eleven written feedback messages on the live site are some form of "cand revine apa calda la blocul meu":

"Cand revine apa caldă in bl.8C pe Ghiță Șerban 14" — /strada/str-sold-ghita-serban
"Cind revine apa calda in Barajul Bistritei 7" — /strada/ale-barajul-bistritei
"Str. Secuilor bl.19 cind se pornește apa calda" — /sector/4
"Nu este actualizat la data de 31.08.2026" — /punct-termic/pt-1-c5-1

CMTEB publishes an estimated restore time and a cause. pipeline/parse.py has always parsed both (remediere_last, cause_raw) and they already ship on every Episode in the bundle — they were only ever rendered on /sector/N. Every one of those people had landed on a street or PT page.

So this PR adds no data. It surfaces what was already there.

What

  • lib/pt-live.tsongoingForPt() collects a PT's still-open episodes, deduped across the year boundary, avarie before programat. Deficiency episodes are excluded: degraded pressure is not a stoppage, and conflating the two is the exact error the headline metric is careful to avoid.
  • restoreQualifier() — adds "azi" / "mâine" / "peste N zile". An absolute datetime reads identically whether the estimate is tonight or a month out. It also surfaces "termen depășit": an estimate that has already passed while the outage is still flagged ongoing is real, common, and otherwise invisible.
  • components/OngoingBand.tsx — renders it, including an honest empty state. Silence would read as a denial to someone whose water is off right now, so the page says plainly that it has no announcement and points at cmteb.ro.

Worth reviewing carefully

Street pages. The band goes inside renderPtPanel, so it follows the block-finder selection with no new client code and pure SSG holds. Verified in a real browser across all six serving PTs of /strada/sos-alexandria: exactly one band visible per selection, index tracking 1:1, never two at once. (Caveat found during verification: five of those six PTs share the same CMTEB announcement, so switching between them looks static — the swap is real, but the visual proof is deceptive.)

No new bundle field, so the deploy-order trap does not apply. Reads stay defensive (?? []) so an older bundle degrades to the empty state rather than throwing across ~1,200 static pages.

Contrast measured, not eyeballed. --color-avarie (#C2410C) is 4.42:1 on paper-2 — under AA. The band uses v-red at 5.70:1.

Checks

12 unit tests, 3 e2e, full suite 37 e2e + 134 unit green, tsc clean. Visually verified headless at 1440×900 and 390×844: PASS both, 0 console errors, band confirmed in the server-rendered HTML.

E2E fixtures are data-driven and skip out of season. Right now 117 PTs carry an ongoing outage with a real restore time, so this lands on real pages immediately.

One product decision left to you

Visual verification measured the restore time rendering at y=903 on a 1440×900 street page — 3px below the fold. The band trails the H1, the range sentence, the block finder and the whole verdict card, so someone whose water is off sees a 2025 statistic first and must scroll for the live answer.

I did not reorder the page: leading with live status instead of the headline number changes what the site says first, which is your call, not mine. Options are to leave it, or to hoist the band above the verdict card only when something is ongoing.

🤖 Generated with Claude Code

Six of the eleven written feedback messages on the live site are some form
of "cand revine apa calda la blocul meu". CMTEB publishes an estimated restore
time and a cause, pipeline/parse.py has always parsed both (remediere_last,
cause_raw), and they already ship on every Episode in the bundle - they were
simply only ever rendered on /sector/N. Every one of those people had landed
on a /strada/ or /punct-termic/ page.

So this adds no data. It surfaces what was already there:

- lib/pt-live.ts: ongoingForPt() collects a PT's still-open episodes, deduped
  across the year boundary, avarie before programat. Deficiency episodes are
  excluded - degraded pressure is not a stoppage, and conflating the two is the
  exact error the headline metric avoids.
- restoreQualifier() adds "azi" / "maine" / "peste N zile", because an absolute
  datetime reads identically whether the estimate is tonight or a month out.
  It also surfaces "termen depasit" - an estimate that has already passed while
  the outage is still flagged ongoing is real, common, and otherwise invisible.
- components/OngoingBand.tsx renders it, including an honest empty state:
  silence would read as a denial to someone whose water is off right now, so
  the page says it has no announcement and points at cmteb.ro.

On street pages the band goes inside renderPtPanel, so it follows the block
finder selection with no new client code and SSG stays pure. Verified in a
browser across all six serving PTs of a multi-PT street: exactly one band
visible per selection, never two.

No new bundle field, so the deploy-order trap does not apply; reads are still
defensive (`?? []`) so an older bundle degrades to the empty state.

Contrast measured, not eyeballed: --color-avarie is 4.42:1 on paper-2, under
AA; the band uses v-red at 5.70:1.

12 unit tests, 3 e2e (data-driven, skip out of season - 117 PTs currently
carry an ongoing outage with a real restore time).

Co-Authored-By: Claude Code <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
termo-site Ready Ready Preview Sep 7, 2026 8:36pm UTC

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid feature, well-tested, and the security-conscious choices (never rendering cause_raw, defensive ?? []/optional-field reads, correctly excluding episodes_deficienta) are all correct. One correctness bug blocks this:

lib/pt-live.ts:36-37 — dedup key is e.start alone, but the PR's own code says that's not unique.

ongoingForPt drops any episode whose start timestamp it has already seen:

if (seen.has(e.start)) continue;
seen.add(e.start);

This is meant to collapse an episode that gets republished under both years at a year boundary. But components/OngoingBand.tsx:55-56 documents, in the same PR, that start is not a unique identifier for an episode: "start alone is not unique: one PT can carry two announcements that begin at the same timestamp" (that's why the React key there also folds in cause_class and array index).

Given that, ongoingForPt's dedup is too aggressive: if a PT genuinely has two distinct ongoing episodes that happen to share a start timestamp (e.g. an avarie and a programat announced simultaneously), the second is silently dropped and never reaches the page — for the exact feature whose entire point is "tell people when their water comes back." The unit tests don't cover this: test/pt-live.test.ts's year-boundary test only exercises the identical-episode-republished-in-both-years case, not two distinct same-start episodes within one year.

Suggested fix: key the dedup on something that actually identifies an episode (e.g. `${start}|${cause_class}` at minimum, ideally including cause_raw or remediere_last), and add a unit test for two distinct ongoing episodes sharing a start to pin the fix.

Minor/non-blocking:

  • OngoingEpisode.cause_raw (lib/pt-live.ts:21) is threaded all the way through but never rendered by OngoingBand — either use it or drop it from the type.
  • OngoingBand's sector prop is typed number | null but both call sites pass PtEntity.sector, which is non-nullable per lib/data.ts:57 — harmless, just looser than it needs to be.

@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown

@claude please address this review:

  • lib/pt-live.ts:36-37ongoingForPt dedups solely on e.start, but components/OngoingBand.tsx:55-56 documents that start is not unique per episode ("one PT can carry two announcements that begin at the same timestamp"). This means a second genuinely-distinct ongoing episode sharing a start timestamp gets silently dropped and never shown. Key the dedup on something that identifies an episode (e.g. `${start}|${cause_class}` at minimum) instead of start alone, and add a unit test in test/pt-live.test.ts covering two distinct ongoing episodes with the same start.
  • Minor: OngoingEpisode.cause_raw is plumbed through but never rendered by OngoingBand — either use it or drop it from the type.
  • Minor: OngoingBand's sector prop is typed number | null but PtEntity.sector (both call sites) is non-nullable — fine to tighten to number.

…ongoing

Conditional, not unconditional. When the water is off right now that outranks
last year's total, but on the ~90% of pages with nothing happening the site
still leads with the number it is known for, and the quiet "no announcement"
line stays below.

The measurement that drove it: at 1440x900 on a street page the restore time
rendered at y=904 against a 900px fold, behind the block finder and the whole
verdict card. Off screen, on the page most people reach from a search, for the
one question the feedback says they are actually asking. Hoisted it renders at
y=528.

Two e2e tests pin the rule in BOTH directions - band above verdict when ongoing
(and the restore time above the fold, not merely above the card), verdict first
when not - so a future layout change cannot quietly undo it.

39 e2e, 134 unit, tsc clean.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@tiXor-code
tiXor-code merged commit 28e5708 into main Sep 7, 2026
2 of 3 checks passed
@tiXor-code
tiXor-code deleted the feat/ongoing-restore-time branch September 7, 2026 20:22

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this PR is already merged, but the correctness bug flagged in the prior review is still present in the code as merged — filing this so a follow-up fix lands.

lib/pt-live.ts:36-37ongoingForPt's dedup key is e.start alone, which the PR's own sibling component says is not unique.

if (seen.has(e.start)) continue;
seen.add(e.start);

components/OngoingBand.tsx:55-56 explicitly documents that a PT can carry two distinct ongoing announcements sharing the same start timestamp — that's exactly why the React list key there folds in cause_class and index, not start alone. ongoingForPt doesn't apply the same care: two genuinely different ongoing episodes (e.g. an avarie and a programat both beginning at the same instant) will collide in seen, and the second is silently dropped before it ever reaches the page — for a feature whose entire purpose is telling people when their water comes back. test/pt-live.test.ts only covers the identical-episode-republished-across-a-year-boundary case, not two distinct same-start episodes within one year, so this ships untested.

Suggested fix: key the dedup on something that actually identifies an episode, e.g. `${start}|${cause_class}|${remediere_last}`, and add a unit test for two distinct ongoing episodes sharing a start.

Minor / non-blocking (also unaddressed from the prior review):

  • OngoingEpisode.cause_raw (lib/pt-live.ts:21) is threaded through but never rendered by OngoingBand — use it or drop it from the type.
  • OngoingBand's sector prop is typed number | null, but PtEntity.sector (lib/data.ts:57) is non-nullable at both call sites — harmless, just looser than it needs to be.

Everything else in the diff checks out: no unescaped rendering of scraped cause_raw, defensive ?? [] reads keep the deploy-order contract, deficiency episodes correctly excluded via the separate episodes_deficienta field, external link uses rel="noopener nofollow", and the hoist-above-verdict ordering change (this PR's second commit) is implemented consistently across both the PT and street pages.

@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown

@claude please address this review:

  • lib/pt-live.ts:36-37: dedup key in ongoingForPt is e.start alone. OngoingBand.tsx documents that start timestamps aren't unique across distinct episodes at a PT, so two genuinely different ongoing episodes sharing a start silently collide and the second is dropped. Key the dedup on something that actually identifies an episode (e.g. `${start}|${cause_class}|${remediere_last}`) and add a unit test for two distinct same-start ongoing episodes.
  • Minor: OngoingEpisode.cause_raw is carried through lib/pt-live.ts but never rendered by OngoingBand — use it or drop it.
  • Minor: OngoingBand's sector prop is typed number | null though PtEntity.sector is non-nullable at both call sites.

This branch was successfully deployed

1 active deployment
Preview 534435a1 Deployed Sep 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant