live status: show the estimated restore time where people actually land - #14
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
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 byOngoingBand— either use it or drop it from the type.OngoingBand'ssectorprop is typednumber | nullbut both call sites passPtEntity.sector, which is non-nullable perlib/data.ts:57— harmless, just looser than it needs to be.
|
@claude please address this review:
|
…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>
There was a problem hiding this comment.
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-37 — ongoingForPt'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 byOngoingBand— use it or drop it from the type.OngoingBand'ssectorprop is typednumber | null, butPtEntity.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 please address this review:
|
Why
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.pyhas always parsed both (remediere_last,cause_raw) and they already ship on everyEpisodein 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.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 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 onpaper-2— under AA. The band usesv-redat 5.70:1.Checks
12 unit tests, 3 e2e, full suite 37 e2e + 134 unit green,
tscclean. 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