Skip to content

Fix what a retroactive review found in what the site publishes - #53

Merged
baz8080 merged 12 commits into
mainfrom
claude/eager-sagan-wn68lr
Sep 24, 2026
Merged

baz8080 merged 12 commits into
mainfrom
claude/eager-sagan-wn68lr

Conversation

@baz8080

@baz8080 baz8080 commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Slice 3 of the retroactive review, following #48, #51 and #52: a whole-file review of esb_site/render.py, site.html, the three static templates, site.css and __main__.py, then three reviews of this PR. Each commit holds one fix, and each fix's test was checked to fail without it.

The fixes

  1. Events over before the first poll were counted outside the county page. Area pages, search, repeat chains and the build summary still saw them: 14 outages, a page for an area whose only event was one of them, and a repeat fault reading "outage 2 of 2" with no outage 1. They are now dropped once, in model.load_outages, by the end the merge settles on and before the chains are labelled.
  2. CSV formula injection. A text cell whose first non-space character is =, +, - or @, or that opens with a tab or carriage return, now gets a leading apostrophe.
  3. --now dropped an offset instead of converting it.
  4. The county row spliced the name into an inline onclick JS string. It now uses data-county and one delegated listener. The area page's link into the app encodes the county.
  5. A malformed share link (#county/%E0) blanked the app. It now falls back to the overview.
  6. "1 customers" is now singular in the timeline and the fault-spots card.
  7. The nearby-area links now use area_path.
  8. A month the data hasn't reached says "no data yet" (owner's call), never zeros.
    • model.month_watched decides it once, and the app gets it as D.nodata.
    • The county table shows the caveat, a grade chip with its reason, and dashes. The gate sentence says "There is no data yet for October 2026", and no grade date ships for that month.
    • The app shows it in the headline, the tiles, the county rows and the county view.
    • model.overlaps rejects an empty window, so nothing is counted or filed in such a month.
    • "So far" belongs only to the newest listed month, and only once data reaches it.
    • Recorded in notes/design-alignment.md, with a CLAUDE.md Settled row.

JS was checked in Chromium with a normal build and one dated 00:20 on 1 October:

  • the headline and basis wording is right for both months;
  • row clicks work and the bad link recovers;
  • there are no page errors;
  • all 856 Dublin and Cork records render identically from JS and Python.

CI runs no JS, so the call sites are held by text tests.

Reviews of this PR

  • First review, 9 findings, all fixed. Chiefly, the first version of fix 1 filtered after the repeat chains were labelled.
  • Second review, 10 findings. Chiefly, "no data yet" used different rules on the page and in the app. It now uses one rule, shipped to the app.
  • Third review, of that fix alone, 8 findings, all fixed.
    • The real bug: the one-microsecond observed_month change left "so far" keyed on it, so at a month start the app called September "so far" while the page had closed it.
    • A structural fix: the empty-window guard was copied into three places; it's now model.overlaps.
    • Smaller fixes:
      • a grade date shipped for a no-data month;
      • the table row was untested and had an unstyled grade cell;
      • an old cached data.js would bring the zeros back;
      • two comments restated the settled decision.

Not taken:

  • A whole month the collector missed reads as watched once data resumes after it. That's the in-window gap notes/grading.md names.
  • The merge filter dropping an event whose lingering sibling ended after the first poll. That's the merge's own rule.
  • Folding the inline plural suffixes into one helper: wider than this PR.

Effect on the site

Every published county and national figure is identical to main. The area directory, area pages and search lose the 14 events over before collection began.

Checks

  • ruff check: clean.
  • unittest discover with ESB_DATA_DIR set: 346 tests OK.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UqWuH6frirpbQDKnF5rEdT

…pages

build dropped outages restored before collection began from each county's
list, as the county page, shard and CSV require, but area_index and the
search index were built from the unfiltered list. areas.html, the area
pages, the county page's area card and search.js counted 14 outages the
county page did not list, and an area whose only event was one of them got
a page and a sitemap entry. One helper now applies the rule everywhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqWuH6frirpbQDKnF5rEdT
county_csv wrote ESB's free-text location, the Census town and the planned
reason as-is, so a cell opening with =, +, -, @, tab or carriage return ran
as a formula in the spreadsheet a reader opened c/<slug>.csv in. Those
cells now get the usual leading apostrophe. No location in the corpus
starts with one today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqWuH6frirpbQDKnF5rEdT
--now was parsed with .replace(tzinfo=UTC), which relabels a timestamp
carrying an offset rather than converting it: 10:00+01:00 built as 10:00
UTC, an hour late, enough to move a day cell or a month boundary. It is
now converted, and a bare timestamp is still read as UTC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqWuH6frirpbQDKnF5rEdT
…rowing

The county row's onclick put the name inside a JS string with esc(), which
the HTML parser undoes before the script runs, so an apostrophe in a name
broke out of the string; ui.js had already dropped inline handlers for this
reason. The name now travels in data-county to one delegated listener. And
route() decoded the hash unguarded: #county/%E0 threw URIError in boot and
the list never rendered (reproduced in Chromium: 0 rows). It now falls back
to the overview. Checked in Chromium: a row click still opens the county,
the bad hash shows all 26, no page errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqWuH6frirpbQDKnF5rEdT
The summary line already handled the singular; the timeline row, in
render.py and its site.html mirror, always pluralised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqWuH6frirpbQDKnF5rEdT
The nearby card rebuilt the a/<county>/<area>.html scheme by hand, so a
change to area_path would have moved the pages and quietly broken every
nearby link. Same target, one definition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqWuH6frirpbQDKnF5rEdT
…ains

The render-level filter this PR added ran after label_repeats, so a fault
following a pre-collection outage at the same spot kept a chain naming a
leg the site lists nowhere, and every other caller of load_outages, the
build summary and its empty-database guard included, still saw those
events. They are now dropped once, in load_outages, after the merge (an
event ends with its last member) and before the chains are labelled; the
render helper goes. Found by review of this PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqWuH6frirpbQDKnF5rEdT
- "up to 1 customers" on the county page's fault-spots card, the same
  plural slip fixed in the timeline.
- _cell checked only the first character, so a formula behind a space or
  newline, which spreadsheet importers trim, went out live; the planned
  reason column is now under test too.
- The area page's link into the app wrote the county into the hash raw,
  the one other producer of #county/ besides go(); it is now encoded and
  escaped.
- The script test now also holds that something listens for the county
  rows' clicks, or a deleted listener would leave every row dead and CI
  green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqWuH6frirpbQDKnF5rEdT
A build just after Dublin midnight on the 1st, from data to the night
before, listed the new month under "to 30 Sep" on the county page, and the
app headline read "0 faults and 0 planned outages" as though the month were
over and quiet. The table's caveat now reads "no data yet", and the app says
so in the headline and the county view and shows dashes, not zeros, in the
tiles and the rows' counts. Owner's call; the note records it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqWuH6frirpbQDKnF5rEdT
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqWuH6frirpbQDKnF5rEdT
… month

Review of this PR found the first cut used two rules: the page tested for an
empty observed window, the app compared months with observed_month, and on a
horizon landing exactly on Dublin midnight they disagreed, the app back to
"so far: 0 faults". model.month_watched now decides it and ships it as
D.nodata, observed_month is taken just before the horizon like partial_days,
and the rest follows:
- the county table shows dashes, not zeros, for such a month
- the gate sentence says there is no data rather than promising a grade date
- nothing is counted, nationally or per county, or filed in the shard for it,
  though a published restore time past the horizon used to slip in
- CLAUDE.md gets its Settled row, and the merge comment says which end the
  collection-start filter uses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqWuH6frirpbQDKnF5rEdT
A review of the last commit found it had moved observed_month a microsecond
back but left the app's "so far" and "this month" keyed on it, so for the
first hours of every month the app called September "so far" while the
county page had closed it. "So far" now needs the newest listed month, with
data in it. Also:
- one model.overlaps rejects an empty window for county_month, the national
  row and the shard, instead of three copies of the guard
- _daygate ships no grade date for a month with no data
- the no-data table row carries the grade chip and its reason, and a test
  holds every row's cell count to the header
- a data.js cached from before `nodata` falls back to comparing months
- comments no longer restate the settled decision

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqWuH6frirpbQDKnF5rEdT
@baz8080
baz8080 merged commit f62497b into main Sep 24, 2026
3 checks passed
@baz8080
baz8080 deleted the claude/eager-sagan-wn68lr branch September 24, 2026 12:48
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.

2 participants