# BidBrain project status, part 13: v3.0, Integrations redesign and hidden connections (26 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.

  - **v3.0 starts here, 2026-08-26 (after the v2.9 tag was cut).** Mark: "im happy to push this as the v3.0 release since its a complete redesign". A full visual redesign, matching a new design system (built first as a standalone mockup) into all four real pages: the cockpit, Purchases, Didn't win and Settings. No gate, pricing or CompMatch logic touched anywhere in this whole run of work, every real feature and its own behaviour is exactly as it was, restyled and reorganised only. Full detail is in `CHANGELOG.md`'s own v3.0 entry (pulled into the GitHub Release notes the same way every version here always has been); the headline pieces: a persistent, collapsible left app nav shared by all four pages (Right Drive's own real logo, chroma-keyed to true transparency; Purchases and Didn't win nested under BidBrain with a down arrow to collapse them; Dashboard/Stock/Customers/Reports shown as real but inert "Soon" placeholders for the future Dealer OS); a new Theme settings tab (brand colour, panel colour, logo and favicon uploads); the filter sidebar on every page became a floating panel opened on demand rather than a permanent column, freeing the card grid back to multiple columns on a 14 inch laptop; real 4:3 card photos; the search box moved out of the Filters panel onto the nav bar itself, next to the refresh icon; Purchases and Didn't win both gained a real per-platform heading matching the cockpit's own, with the old permanent summary tiles moved into a Stats dropdown; every icon across all four pages now reads in the same order with the same plain look, every remaining glyph or CSS pseudo-element icon replaced with a real inline SVG. Four real bugs found and fixed along the way, all live-verified: `.insetpanel`'s own `overflow:hidden` was silently breaking the top bar's `position:sticky` since the app shell was first built (moved the rounded-corner styling onto `.controls` itself instead); `daily_run.py`'s settings cache is correct for its own short lived CLI runs but was wrong for `serve.py`'s long running process, so a saved theme change only ever took effect on the very first render after a restart; Didn't win's own new heading/Stats/Filters CSS had silently never landed on that page at all, since the edit was anchored to text only purchases.html happened to share; and the red notification badges were cutting the number off at the bottom of the circle, `box-sizing:border-box` shrinking the badge's own content area below what its `line-height` assumed, fixed with flex centring instead. Also carries a small handful of real features that had shipped since v2.9 but were sitting unreleased: a real Cazana deep link (the "Cazana" label plus a dedicated button under Valuation detail, linking to that exact car's own live Percayso search result) and a "Due in" (collection date) column on the purchases page, read off Motorway's own delivery timeline. The README's Roadmap section was updated the same way as every release: "Path to v3", mapped out 2026-08-25 before the redesign itself became v3.0, is now "Path to v3.1" instead, the goal carrying over unchanged.

  - DEALERKIT LOGIN AUTOMATED TOO, THE SAME WAY AS CAZANA, CARWOW AND
    MOTORWAY, 2026-08-26 (Steven, right after confirming which logins were
    already automated: "do the work on dealerkit now so it works the same
    as cazana"). Checked the real login page live first, same discipline as
    every other one of these: a plain username/password form
    (input[name=username]/input[name=password], no stable id to rely on),
    a real type=submit Login button, no CAPTCHA visible.

    `setup_credential.py` gained "dealerkit" as a fourth site, but unlike
    the other three it cannot be a fixed entry in SITES: DealerKit's own
    hostname is dealer specific (a different tenant subdomain per
    dealership), so a new `_dealerkit_server()` reads it fresh from
    `dealer_config.DEALERKIT_BASE_URL` (the same value
    `bidbrain.browser.SITES["dealerkit"]` already uses) rather than
    hardcoding Right Drive's own subdomain into the shared script.

    New `auto_login_dealerkit.py`. One real structural difference from
    Motorway, worth remembering: DealerKit is like Carwow, not Motorway.
    Its own login cookie is a genuine browser session cookie (`expires=-1`,
    found live 2026-08-24), so it never survives a fresh process's own
    Chrome launch, every DealerKit read and write already uses
    `browser.open_reader_context` (the saved `data/dealerkit_state.json`),
    never the shared persistent profile. So this script logs in through a
    fresh, throwaway context (the same shape as `auto_login_carwow.py`) and
    saves the result to that same state file, no persistent-profile timing
    conflict to worry about the way Motorway's own script has to avoid.
    Verifies success the same real way `daily_run.dealerkit_pass()` already
    does: land on the real stock list URL and wait for a genuine
    `.vehicle-title` element, never guessed at a different signal.

    Scheduled as `com.bidbrain.dealerkit-login` at 17:00, alongside Cazana
    and Carwow's own jobs (safe together since none of the three touch the
    shared persistent profile the way Motorway's 06:00 job has to avoid).

    Verified: the "no saved credential yet" path fails with a clear,
    actionable message (confirmed live, no credential stored yet at build
    time, matching Motorway's own first build); the new plist passes
    `plutil -lint`, loads cleanly, and `launchctl print` confirms the real
    registered 17:00 calendar interval. Not yet proven with a real login,
    same caveat as every one of these on its first build, Steven still
    needs to run `setup_credential.py dealerkit` himself, the credential
    step no session can do on his behalf.

    Steven then ran `setup_credential.py dealerkit` and confirmed
    `auto_login_dealerkit.py` genuinely worked: a real authenticated stock
    read straight after login returned 45 vehicles, 33 model keys, no
    manual login needed at all.

  - LOGIN STATUS TEXT AND HEALTH DOT FIXED TO STOP CONTRADICTING EACH
    OTHER, 2026-08-26, same day. Steven caught it live on the Integrations
    page from a screenshot: DealerKit showed a RED health dot right next
    to "Logged in within the last day", and separately Motorway showed a
    GREEN dot right next to "No saved login yet on this machine", both
    nonsensical read together. Investigated properly rather than guessed,
    reading `_login_status`/`_site_flag`'s own source and querying
    `db.get_site_health()` directly before touching anything, and found
    two genuinely separate causes, not one:
    (1) The health DOT (`_site_flag`, `db.get_site_health()`) only ever
    tracked whether the last real DATA READ succeeded, never whether a
    LOGIN succeeded on its own; none of the four `auto_login_*.py`
    scripts had ever called `db.record_site_health` on a successful
    login, so a stale FAILURE record from before DealerKit's login was
    fixed (a real one, timestamped from before this session's build, "the
    API returned its sign in page instead of real data") sat there
    showing red indefinitely, regardless of how fresh the login actually
    was, until some later real data read happened to come along and
    clear it. Fixed by adding `db.record_site_health(<site>, True, "")`
    to all four scripts' own success paths (Cazana, Carwow, Motorway
    including its "already logged in, nothing to do" shortcut, and
    DealerKit), each verified to correctly clear a real stale failure
    record when re run.
    (2) `_login_status()`'s own TEXT was purely based on a saved session
    state file's own mtime (`browser.state_path(site_key)`), which
    structurally can NEVER exist for Motorway by design (it uses the
    shared persistent browser profile, not a state file, see
    `auto_login_motorway.py`'s own docstring), so it always said "No
    saved login yet on this machine" for Motorway regardless of how
    healthy it actually was, a real structural false negative baked in
    from the very first build of this page, not something DealerKit's
    fix touched. Investigated whether Chrome's own internal profile files
    (`data/browser_profile/Default/Cookies` etc) could serve as a
    reliable login-freshness proxy instead, checked live via `find ... |
    sort -rn`, and rejected it: those files update on ANY Chrome launch
    or activity, not specifically a successful login, so their mtime
    would be just as misleading. Fixed instead by having
    `_login_status()` fall back to the SAME `site_health` record the
    health dot itself already reads, whenever no state file exists, now
    that logins genuinely write to it too, so a persistent-profile site
    gets an honest, real signal instead of a hardcoded "never" message.
    Verified live end to end on the real Settings > Integrations page,
    not just the unit level: DealerKit now shows a green dot with
    "Logged in within the last day"; Motorway now reads "Logged in
    within the last day (confirmed by a recent login)" instead of the
    old false "No saved login yet"; Carwow, Auction4Cars and DealerWay
    (all state-file based, untouched by either fix) still correctly read
    green with a real login age; Dealer Auction's own red dot and "No
    saved login yet" are untouched and still correctly agree with each
    other, that one is a real, separate, still unresolved login issue,
    not something either of these two fixes was meant to address.

  - INTEGRATIONS TAB REDESIGNED, ONE MERGED HONEST CONNECTION LIST,
    2026-08-26, same day. Steven asked for a proper review of the
    Integrations page ("is this the best most functional most easy to use
    way to display and manage the integrations"), given without making any
    changes first. The real findings: the page had two separate,
    unlabelled signals per site (the login-freshness text just fixed above,
    and the last-read health dot) that could say contradictory things even
    after that fix's own narrow scope; nothing distinguished a genuinely
    broken login from a healthy one, so a real failure sat visually equal
    to seven healthy rows anywhere on the page, including the very bottom;
    the three cards (Integrations, Valuations, Auction house logins)
    grouped by how the code happened to grow, not by what a user comes to
    the page to do; every card's own explanatory prose sat permanently
    open, pushing the actually useful state text down; the toggles read as
    checkboxes saved via a separate, easy to forget "Save integrations"
    button; and nothing distinguished the four sites that now log
    themselves back in automatically (Cazana, Carwow, DealerKit, Motorway,
    see the auto_login_*.py entries above) from the five that still need a
    person. Mocked up as a standalone interactive HTML page first (sent to
    Steven, not built into the app), matched against his real live data;
    he confirmed the mockup, keeping the tab named "Integrations" (not
    renamed "Connections" as first proposed) and moving Software update to
    the Overview tab, then said to build it.
    - `bidbrain.render._site_state(site_key)` replaces the old separate
      `_login_status`/`_site_flag` functions outright: one merged, honest
      sentence per site (a real read failure wins over a merely stale
      session file; a site with neither a saved session nor a single read
      attempt on record reads as grey "Not set up on this Mac yet", not a
      fault; otherwise "Working" plus how fresh, plus, for the four
      automated sites, "Logs itself back in daily at HH:MM"). A failure's
      raw error text is capped to 140 characters inline (the real Dealer
      Auction message carries a huge OAuth URL that would have wrecked the
      row's own layout), the full text kept in a hover title.
    - New `_SITE_AUTO` (which four sites are automated and at what time,
      read from the real launchd schedule already documented above, not
      re derived at render time) and `_SITE_ABOUT` (one plain sentence per
      site, tucked behind a new ⓘ info button per row, collapsed by
      default, `toggleConnNote(btn)`).
    - The three old cards and their per site loops (`_login_block`,
      `_integ_row`, `_PLATFORM_LOGINS`) are gone, replaced by one row
      builder over `_CONN_ORDER` (all nine sites in a fixed canonical
      order), sorted at render time: broken first (`dot == "bad"`), then
      auto-login sites, then manual ones, then never-connected grey ones
      last, under a real attention banner naming whichever sites are down
      (`"N connections need your attention. X, Y."`), present only when
      something actually is.
    - DealerKit's two sub-features and Glass's and LE Capital's own single
      one (from the unchanged `_INTEGRATIONS` list) now render as real
      sliding switches (`.switch`, CSS only, same underlying
      `<input type="checkbox" id="integ-{key}">` `paintIntegrations()`
      already painted) and apply instantly on `onchange="saveIntegrations()"`,
      no separate Save button on this tab any more (`saveintegrationsbtn`
      removed from the top bar, `switchView`, and `load()`'s own wiring).
    - A new waiting strip (`#loginwait`, reusing `.conn-banner`'s own look
      in a blue variant) explains, in plain sight, why every other login
      button just greyed out while one is open, the exact confusion Mark
      hit live on 2026-08-24 ("it wont allow me to click it") with nothing
      on screen to say why; wired into the existing `paintLoginButtons`
      poll loop, no new mechanism.
    - Two real bugs found and fixed live while building this, both on the
      very first real load, neither invented, both genuinely useful since
      DealerKit and Dealer Auction were both actually down on this Mac at
      the moment of testing: (1) the JS `SITE_LABELS` dict used by the
      relogin confirm dialog and the new waiting strip was missing
      "cazana" entirely, a pre-existing gap this change's own testing
      happened to surface, would have shown "undefined" in the confirm
      dialog for Cazana specifically; added. (2) `.conn-banner`'s own
      `display:flex` rule beat the browser's own `[hidden]{display:none}`
      rule at equal specificity (ours loads after the UA stylesheet), so
      the waiting strip rendered as a visible, empty rounded pill on the
      very first real screenshot despite genuinely carrying the `hidden`
      attribute; fixed with an explicit `.conn-banner[hidden]{display:none}`
      override, verified via `getComputedStyle` before and after.
    - Software update moved from the Integrations panel to the top of the
      Overview panel (Steven's instruction), its own markup and
      `.integ-status`/`.relogin-btn` styling untouched, just relocated.
    - Verified live end to end against the real running server, not just
      the unit level: the real sort order was proven with two genuine live
      failures (DealerKit's saved login had died again, Dealer Auction's
      still had not been fixed) correctly floated to the top under a real
      banner reading "2 connections need your attention. DealerKit, Dealer
      Auction."; the info toggle; an instant switch flip on Glass's own
      sub-feature persisting to `/api/settings` (confirmed via curl) and
      restored to its real value afterward; and the waiting strip showing
      the right site name and disabling every other login button while
      simulated active, then clearing correctly once cleared. 528 tests
      pass (this page has no dedicated unit coverage, verified live only,
      matching its established pattern).

  - LOGIN.PY'S OWN MANUAL RELOGIN FLOW NEVER CLEARED A STALE HEALTH
    FAILURE, FOUND AND FIXED, 2026-08-26, minutes after the redesign
    above shipped (Steven: "no matter how many times i try to login to
    dealerkit... i click im logged in button and press save, it doesnt
    work"). Investigated live rather than guessed: `data/dealerkit_state.
    json` had a genuinely fresh mtime (matching a login moments earlier),
    and a real headless read using that saved session succeeded (real
    vehicle titles came back), so the login itself was never broken. The
    real cause: `login.py` is the SHARED script behind every one of the
    nine sites' own "Log in to X" button on the Integrations tab (`/api/
    relogin` always runs `login.py <site> manual`), and it never called
    `db.record_site_health` on a successful login, only daily_run.py's own
    platform reads and, from the day before, the four `auto_login_*.py`
    scripts did. So a stale failure recorded before Steven's login just
    sat there showing red forever on the new redesigned page, however
    many times he genuinely logged back in through this exact flow, since
    `_site_state()` (built the same day) correctly trusts a real failure
    record over a merely fresh session file, and nothing was ever telling
    it the failure was over. `login.py` now records a clean health result
    right after `ctx.storage_state()` saves, but only when a person
    actually confirmed via the "I'm logged in" flag (every relogin
    triggered run, since `/api/relogin` always passes "manual") or the
    three sites with real auto detect (Motorway, Carwow, Auction4Cars),
    never on a bare 600 second timeout with nobody confirming anything.
    DealerKit's own already stale record was cleared directly the same
    moment, since the session had already been independently verified
    working before the fix landed, so Steven does not have to repeat the
    exact flow that was already correct just to see the page catch up.
    Verified live: the Integrations tab immediately read "Working, logged
    in today, logs itself back in daily at 17:00" for DealerKit, only
    Dealer Auction (a real, separate, still open issue) left showing red.
    528 tests pass (login.py has no dedicated unit coverage, this whole
    fix verified live, matching the file's own established pattern).

  - HIDE UNUSED CONNECTIONS, A WAY BACK, AND A RUN MODAL THAT OMITS THEM,
    2026-08-26, same day (Steven, straight after the redesign: "there is
    no way to hide the ones we dont use... if we hide them how do we get
    them back if we do want to start using them? and... in the daily run
    on the main page... if i set it up that i have hidden it or dont want
    it to autorun (as a side note is this set up) that it wont even show
    up in the modal?"; the side note answer was yes, the "Read this
    platform" toggle on each platform's own Settings tab already excludes
    it from both scheduled and manual runs, built 2026-08-25).
    - Hiding is deliberately more than cosmetic: `serve._apply_hide(site,
      hidden, saved)` (pure, unit tested) flips the site's own REAL off
      switches with it, a platform's "Read this platform" toggle, an
      integration site's own enabled states (DealerKit's two together, LE
      Capital's funding sync, Glass's on demand valuation), so "hidden"
      genuinely means "we don't use this" everywhere the existing
      switches are already respected (the scheduled run's
      `_select_platforms` floor, Run now, every gated endpoint), with no
      new enforcement mechanism invented. Showing it again flips them
      back on, matching Steven's own framing of unhiding ("if we do want
      to start using them"). Stored as a `hidden_connections` list in the
      same settings key value table, normalised to known sites in
      `_merged_settings`. New `POST /api/hide-connection`, which also
      rebuilds cockpit.html best effort (same pattern as `_settings`).
    - Cazana alone cannot be hidden, refused server side with a clear
      error and no hide link rendered on the page: every run prices on it
      and it is the one site with no off switch anywhere, so a
      hidden-but-still-running Cazana would just be a lie.
    - The way back: hidden connections stay on the Integrations tab in a
      muted "Hidden, not used by this dealership" section at the very
      bottom, one "Show again" click each; every visible row except
      Cazana carries a quiet "Hide, we don't use this" link.
    - The Run modal now omits a platform that is hidden OR switched off
      in Settings ENTIRELY. This SUPERSEDES the same day's earlier greyed
      "Off in Settings" row treatment (Steven's own morning ask, "i need
      the settings on the auto runs to be visible"), his newer
      instruction explicitly covering both cases; the Settings tabs
      remain where that state is seen and changed. Mechanically the rows
      are still RENDERED (hidden attribute plus a disabled, unticked
      checkbox so rmToggleAll/rmSync/confirmRun's existing
      `:not(:disabled)` selectors exclude them), never omitted from the
      markup, because cockpit.html is a static file: `openRunModal()` now
      re-syncs the rows and the automation buttons live from
      `/api/settings` every time the modal opens, so a change made since
      the last render applies immediately in BOTH directions (a row
      omitted outright could never reappear after an unhide without a
      full re render). A hidden site's own Run modal buttons (Sync LE
      Capital funding, Run Glass's checks, DealerKit stock and purchases)
      vanish with it, a stronger tier than merely toggled off, which
      stays visible but disabled explaining itself. Enforcement never
      depends on any of this display, the server side floor still
      applies to whatever the modal sends. The dead `.runmodal-opt.off`/
      `.runmodal-opt-note` CSS from the superseded treatment was removed
      outright per house style, and the `[hidden]`-vs-`display:flex`
      specificity guard was applied to the modal rows preemptively, the
      exact bug class caught live on the waiting strip earlier the same
      day.
    - Notices follow the hiding: `_platform_health_notices` skips hidden
      sites (same reasoning as switched off platforms, the recorded
      failure may be exactly why it was hidden), and
      `_session_health_notices`' two "Click Run Glass's checks" prompts
      are now gated on the Glass's toggle (which hiding flips), pricing
      on Cazana alone IS the configured state then, not a problem to
      nag about, and the button being pointed at is disabled or gone.
    - Verified live end to end against the real server: hid LE Capital
      (the genuine not-used-on-this-Mac case) through the real endpoint,
      its funding sync flipped off and its row moved to the Hidden
      section; Cazana's hide correctly refused; briefly hid DealerWay and
      watched its modal row disappear on a real `openRunModal()` sync,
      then showed it again and confirmed its read flipped back on;
      confirmed Dealer Auction's own absence from the modal comes from
      its genuine, pre existing off-in-Settings state on this Mac (the
      new rule working on real data, not a test fixture). LE Capital was
      LEFT HIDDEN on Steven's copy, it is the real unused connection his
      complaint was about, one click brings it back. 539 tests pass (11
      new, `_apply_hide`'s flips both directions, DealerKit's two
      automations flipping together, isolation from unrelated settings,
      list dedup, and both refusals).
    - Postscripts, same day: (1) settings page reloads (hiding a
      connection, "I'm logged in, save it", the software update) used to
      dump back on the Overview tab, since the current tab only ever
      lived in a JS variable; `switchView` now writes `?tab=` to the URL
      via history.replaceState so the existing deep link reader restores
      it on any reload (Steven: "every time i change a setting inside
      settings it changes the page and sends me to overview"). (2) Two
      session health banner tests started genuinely failing minutes after
      the Glass's prompt gate shipped, because Steven had already hidden
      Glass's and Dealer Auction on his own live copy and those tests
      read the REAL settings through `daily_run._get_settings()`'s cache:
      the suppression was working exactly as designed, the tests were
      just never isolated from live state. Fixed by pinning
      `daily_run._settings_cache` around that section (and resetting to
      None after), plus a new test proving the prompt IS suppressed when
      Glass's is off. 540 tests pass. Worth knowing when reading this
      Mac's state later: Steven's own real hidden list is now lecapital,
      dealerauction AND glass, his own deliberate choices through the
      page's confirm dialog, hiding Glass's also switched off the Run
      Glass's checks automation, so all pricing is Cazana only on this
      copy by his own configuration.

