# BidBrain project status, part 7: v2.4.2 (24 August 2026)

Moved unchanged from CLAUDE.md on 2026-09-18. The standing rules stay in CLAUDE.md, which lists every part of this log.

**v2.4.2 starts here, 2026-08-24 (after the v2.4.1 tag was cut).**
  - THE OTHER FOUR SITES SORTED THE SAME WAY, WHICH SURFACED A REAL "NOT WORKING" REPORT THAT TURNED OUT NOT TO BE A BUG. Same day, Mark then worked through Motorway, Carwow, Auction4Cars and Dealer Auction one at a time through the exact same chat flow (open the window, he logs in, tells Claude, Claude calls /api/relogin-done). Partway through he reported "the carwow, a4c & dealer auction login buttons arent working"; investigating rather than assuming, `/api/run-status` showed a real `login.py motorway manual` process already running (login_running: true, login_site: "motorway"), started about 2 minutes earlier, correctly blocking the other three per the shared busy lock built in v2.4. Confirmed with Mark via a quick question rather than silently killing what might have been his own real in progress login: it was, he had simply clicked several buttons in quick succession. All seven sites (dealerkit, glass, lecapital, motorway, carwow, auction4cars, dealerauction) ended the day with a genuine fresh `data/*_state.json`, confirmed via both `ps` (no stray login.py processes left running) and the settings page (every card reading "Logged in within the last day"). No code changed for this part, the mechanism built in v2.4/v2.4.1 handled all seven identically, this was purely using it for real.
  - A SEPARATE, GENUINE MISUNDERSTANDING CAUGHT AND CLARIFIED: Mark then asked "make this the same for LE and dealerkit login & glass's", which read at first like a missing feature. Rather than guess and build the wrong thing, asked directly what "this" meant; his answer, "the kill the login page after logged in", turned out to already be exactly how LE Capital, DealerKit and Glass's worked too, proven with the same evidence (their own state files' timestamps, no stray processes), since `_relogin`/`_relogin_done` have never had any per site special casing, the mechanism is fully generic across all seven sites already. Nothing built, a live demonstration offered instead. Worth remembering: two AskUserQuestion clarifications in one short exchange (the Motorway block, then this) both turned out to be "already working as designed", a useful sign that when a report does not match the code's own logic, checking the real live state first (here, `ps` and `/api/run-status`) resolves it faster than guessing at a fix.
  - A REAL NOTIFICATION WHEN A SITE NEEDS RELOGIN, added the same day (Mark: "are you able to poll the site to add a notification when its required to relogin?"). Deliberately NOT a background poller: Glass's single account wide session has repeatedly shown that even just checking whether a session is still alive can evict the real one (see the 2026-06-15 root cause entry earlier in this file), so a timer that logs into every site every few hours would reintroduce exactly that risk for no real gain. Presented three options (piggyback on real run failures only, an on demand "Check logins" button, or a genuine scheduled poll) and Mark picked the first, the recommended, lowest risk one: nothing new ever logs into anything, this only reports what a run that was already happening found out.
    - `bidbrain/db.py` gained `get_site_health()`/`record_site_health(site, ok, message)`, a thin layer over the existing generic settings key value store (a new `"site_health"` key, no schema migration), storing only the LATEST outcome per site, never a history, so a later clean read silently clears an earlier failure with no manual dismissal needed.
    - `daily_run.py`'s four platform readers (`_read_motorway`, `_read_carwow`, `_read_auction4cars`, `_read_dealerauction`) each now call `db.record_site_health` on both their own success and failure paths. Motorway is the one exception among the four that has no try/except of its own (golden rule 5, fail loud, a dead Motorway login should still stop the run rather than ship a half read list), so it gained a thin try/except purely to record the failure before re raising, the run's own fail loud behaviour is otherwise completely unchanged. `lecapital_funding_pass()` gained the same recording around its own LE Capital login timeout. A new `_platform_health_notices()` reads `db.get_site_health()` and turns any site currently recorded as failed into a plain sentence naming that site, wired into `_write_pages` alongside the existing Cazana/Glass's/DealerKit stock notices.
    - `bidbrain/render.py`'s `_categorize_notices` gained a fourth bucket, `platform`, keyed off each site's own name in the notice text (motorway, carwow, auction4cars, "dealer auction", "le capital"), folded into the single visible Run now button's own red dot alongside the existing DealerKit and Glass's notices (these four platforms have no button of their own, they are checkboxes inside the same Run modal, so the one shared button is the only place a flag can sensibly go) and always shown in full in the bell dropdown regardless.
    - Verified live: manually recorded a fake Carwow failure (`db.record_site_health("carwow", False, "Timeout waiting for the stock list.")`), re rendered the cockpit with `--render-only`, confirmed the real notice text appeared both as the Run now button's own title and as a red bellnote in the bell dropdown; recorded a clean read for the same site and re rendered again, confirmed the notice disappeared completely with no leftover state. 13 new tests (the db round trip: empty, recorded, independent per site, a later success overwriting an earlier failure; `_categorize_notices`' new platform bucket correctly separating a platform notice from an unrelated Glass's one while both still count as alerts). 309 tests pass. While investigating this a genuine, unrelated confusion surfaced: this session's own repeated "296 tests pass" figure, stated several times earlier today, was simply wrong, checked directly against the v2.4 and v2.4.1 tags themselves (both already ran 301, confirmed by checking out each tag's exact test_pricing.py and running it), not a regression or a flaky test, just a slip in an earlier turn's own count that never got caught until this one.
  - BELL NOTIFICATIONS MADE CLICKABLE, straight to whatever fixes them, same day (Mark: "make the click of notification go to the action that is required eg setting page etc"). `_bell_note` now matches on each notice's own already known wording (every notice here is written by this codebase, never user input, the same safe assumption `_categorize_notices` already relies on) rather than guessing: a notice mentioning "Settings > Integrations" (the new platform login ones from the entry above) renders as a real link to `settings.html?tab=integrations`; one mentioning "Run Glass's checks" or "Run DealerKit stock" renders as a link that closes the bell panel and calls the existing `openRunModal()` so the right button is right there to click. A notice with no known destination (Cazana's, which has no relogin button yet) stays plain text rather than link somewhere that would not actually help.
    - Needed a real deep link into a specific settings tab, which did not exist: `settings.html` gained `?tab=` support, read on load and passed straight to the existing `switchView()` (falls back to Overview, unchanged, if the tab name is missing or not one of the five real ones).
    - A REAL BUG CAUGHT BUILDING THIS, not by guessing, by clicking the link in a real browser: `settings.html?tab=integrations` 404'd. `serve.py`'s own GET handler matched `self.path == "/settings.html"` and `self.path == "/purchases.html"` exactly, and a real browser request for either page WITH a query string never equals that bare string, so both routes have silently 404'd on any query string since the day they were written, this is the first time either was ever linked to with one attached. Fixed by matching on `self.path.split("?", 1)[0]` instead for both. Verified live: the exact link that 404'd before now opens the settings page with the Integrations tab already selected.
  - A RED FLAG ON THE SETTINGS PAGE ITSELF, same day (Mark: "add a red flag next to action in the setting page as well"). Each site's own heading on the Integrations tab (both the automations card and the new Auction house logins card) now carries a small red dot, via a new `_site_flag(site)` reading the exact same `db.get_site_health()` record the cockpit's own bell already reads, with the failure message as its hover title. Deliberately independent of a site's own login age shown right underneath it ("Logged in within the last day" can be genuinely true at the same time as a real read failure, a stale session file is not the same fact as a read that just failed, both are shown honestly rather than one overwriting the other). Verified live: the same simulated Carwow failure used to prove the bell notice showed the red dot correctly next to "Carwow" on the Auction house logins card while every other site stayed clean; cleared it afterward and confirmed the dot disappeared. 309 tests pass (this round is render only markup, no new logic to unit test beyond what `_platform_health_notices`/`_categorize_notices` already cover).
  - CARD DETAILS: A PERSONAL "SHOW EVERYTHING" PREFERENCE, 2026-08-24, Steven, on his own migrated copy (see the reconnect entries above). He found the traffic light icon redesign's per card chevron ("Mark 2026-08-20: was one line of text per flag... icons in a row save the space") genuinely costly to use for comparing cars side by side: expanding a card's flag notes, or its Valuation detail, only ever affected that ONE card, so seeing the same figures across several cars at once meant clicking every card individually. He also wanted some of these figures visible by default, as his own choice, without that becoming the shared default Mark's copy (or a future new user) gets. Asked directly whether "reveal the same row" was the right target: it is not, and this was explained rather than built, because the grid is `repeat(auto-fill,minmax(min(480px,100%),1fr))`, so row membership is a function of viewport width and active filters, not a fixed group of cards, resize the window or filter the list and "the row" changes; a global toggle is the only version of this that means the same thing every time.
    - Built as a genuinely personal, client side preference, not a server side default: a new "Card details" block in the sidebar (two checkboxes, "Show flag notes on every card" and "Show valuation detail on every card", render._insight_rows's existing per card .hf-flags-full/.hf-vals-grid toggle machinery untouched underneath). Checking either one expands that kind of detail on all 110 cards at once via applyDisplayPrefs(), and the choice is remembered in localStorage (bidbrain_display_prefs, read and applied on every page load), so it survives a reload and a fresh run's --render-only rewrite without ever touching the server, the database, or Mark's own copy. Deliberately two independent checkboxes rather than one, since "some of these figures" was Steven's own wording, not all or nothing. A card's own chevron still works underneath exactly as before for a one off peek or a one off collapse, the checkbox only sets what state everything starts in.
    - A REAL BUG FOUND AND FIXED while proving this actually works, not by trusting a clean render. First working looking version: checked the box, saw all 110 cards expand live in the browser, looked done. But a real reload (not just the first click) silently reverted to Mark's original collapsed default even though localStorage genuinely held the saved preference, confirmed by reading it back directly. Root cause, found by binary search with temporary console.log calls and a headless Playwright reproduction (page.on("pageerror")/page.on("console"), a real fresh goto then localStorage.setItem then page.reload() in the SAME browser context, the only way to prove restore on load rather than restore within one already loaded session): a JavaScript temporal dead zone bug. The code stored the storage key as const DISPLAY_PREFS_KEY = 'bidbrain_display_prefs' declared textually AFTER the small init IIFE that reads it (the init runs early in the page's script, right after updateModelOptions(), while the const line sat much further down near toggleFlags), so calling the function that reads it, EVEN THOUGH the function itself is a hoisted declaration, hits DISPLAY_PREFS_KEY while it is still in the temporal dead zone and throws ReferenceError: Cannot access 'DISPLAY_PREFS_KEY' before initialization. That error was completely invisible because the reading function's own try...catch, written defensively for a genuinely different case (corrupt or missing storage), silently swallowed it and returned empty defaults instead, which is indistinguishable from "nothing saved yet" unless you go looking. node --check on the generated script (extracted from the served HTML, not the Python source, since a broken template substitution would not show up in ast.parse'ing the .py file) found nothing, because the script was syntactically valid the whole time, this was a runtime ordering bug, not a syntax one. Fixed by removing the module level const entirely and inlining the literal string at both call sites, which deletes the ordering dependency outright rather than just reordering around it (reordering would have worked too, but a future edit could just as easily reintroduce the same trap). Verified with the same Playwright harness across three real scenarios in one continuous browser context: a genuinely fresh visit with nothing saved (both boxes stay unchecked, all 220 detail blocks across 110 cards stay collapsed, matching Mark's shared default exactly), both saved true (both boxes re check themselves and all 220 blocks unhide on reload), and one saved true and one false (only the matching 110 blocks unhide, the independent checkbox stays correctly separate). Zero page errors in the final version. 317 tests pass (render only, no gate or pricing logic touched). Not yet pushed to the shared repo, sitting as a local commit pending Steven's OK, since it is a genuinely generic feature Mark's own users could want too, not something Right Drive specific.
    - REDESIGNED THE SAME DAY, Steven's follow up feedback: the sidebar checkboxes were the wrong shape entirely, he wanted the existing per card down arrows themselves to do this, not a separate control panel: "when you click on the little down arrows on the cards it auto shows for all of the cars until you close them." Rebuilt: the "Card details" sidebar block is GONE, toggleFlags()/toggleVals() no longer take a btn and no longer scope to `btn.closest(...)`, clicking ANY card's own chevron now flips a module level showFlagsAll/showValsAll boolean and repaints every card at once (the exact same all cards loop as before, renamed paintDisplayPrefs, just triggered by the arrow instead of a checkbox); the two kinds stay independent exactly as before, and the choice still survives a reload via the same localStorage key. Caught, before it ever shipped, the EXACT SAME temporal dead zone class of bug as the entry above, in a new place: `let showFlagsAll = false, showValsAll = false;` was first written down near toggleFlags/toggleVals (i.e. after the init IIFE that assigns to them, right where updateModelOptions() runs), so the init IIFE's plain `showFlagsAll = !!p.showFlags;` would have hit the TDZ and thrown, this time completely uncaught (unlike the const key bug, nothing here happens to wrap that specific line in a try/catch). Fixed the same way the lesson said to: moved the `let` declaration to sit directly above the init IIFE, not down near the functions that use it later, with a comment on the spot pointing at this exact prior mistake so it does not happen a third time. Verified live with a full Playwright pass, not just a screenshot: fresh load collapsed on all 96 cards and no leftover sidebar block; clicking card #2's own flags arrow expanded flag notes on all 96 cards and rotated all 96 chevrons while leaving valuation detail untouched; clicking a DIFFERENT card's (the last one's) flags arrow collapsed all 96 again; clicking a vals arrow independently expanded valuation detail on all 96 while flags stayed collapsed; and, the one that actually matters given the bug just found, setting the state then a real page.reload() in the same browser context correctly restored it with zero page errors. 317 tests pass (render only, no gate or pricing logic touched).
