# BidBrain project status, part 17: v3.2, watchlist sync and Under Offer (27 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.2 starts here, 2026-08-27 (after the v3.1.4 tag was cut).**
  - v3.2 RELEASED, THE FIRST TIME THIS SESSION MET A GENUINE DIVERGED
    BRANCH, 2026-08-27 (Mark: "lets push a new verison to v3.2"). This
    local checkout's own HEAD was still sitting at 2793ba5 with the
    entire session's work uncommitted on top, while Steven's own
    concurrent session had already pushed real work to origin (v3.1.4:
    a shared progress bar on the purchases page, sortable columns
    there, "master" learned into VAN_MODELS from a hide note),
    confirmed live via `git log v3.1.4..HEAD` and `git merge-base
    --is-ancestor` rather than assumed clean.
    - Committed this session's own work first, as its own real commit
      (18 files, everything from the Comfortable/Compact icons and page
      switcher through the appnav cleanup, verified secret free by
      inspecting `git status`, `dealer_config.py` correctly stayed
      gitignored), THEN merged `origin/main` in, rather than force
      anything or pick a side blind. CLAUDE.md and `bidbrain/render.py`
      both merged cleanly on their own (git's own line based merge
      handled the append-only CLAUDE.md and the two sessions' mostly
      orthogonal render.py edits without help); `purchases_run.py` hit
      one real conflict, both sides had touched `read_all()`'s own
      docstring and `platforms` default line, resolved by keeping
      Steven's real `_progress()` plumbing (built the same day, "there
      is no animation, or progress bar... i keep thinking its timed
      out") and extending the same per item progress reporting to the
      new Dealer Auction gallery block this session had just added,
      rather than picking one side's version over the other.
    - Verified the merged state properly before ever tagging anything:
      all four pages (cockpit, purchases, Didn't win, settings) re
      checked with `node --check` against their real generated script
      blocks (clean, no leftover `{{TOKEN}}`), all four confirmed
      serving a real 200 from the restarted live server, and the full
      suite re run post merge, 582 of 582 (up from 579, Steven's own 3
      new VAN_MODELS tests folding in cleanly alongside this session's
      own).
    - CHANGELOG.md gained a real v3.2 entry (Auction4Cars and Dealer
      Auction purchases, the DealerWay logo, the view listing link, the
      DealerKit fraction badge, the nav standardisation and the appnav
      cleanup); README's own roadmap section, "Path to v3.2" since
      2026-08-25, rolled forward to "Path to v3.3" with the now shipped
      Auction4Cars/Dealer Auction purchases readers and DealerWay logo
      moved out of it, only DealerWay's own missing purchases reader,
      the four platforms' missing lost bids readers, and Auction4Cars/
      Dealer Auction's still thinner purchase capture (no payment
      breakdown, documents, retail estimate or DealerKit push yet)
      left in scope; a second, unrelated stale heading ("not blocking
      v3.1", never updated when v3.1 itself shipped) corrected to v3.3
      in the same pass.
    - Tagged `v3.2` (annotated), pushed both the branch and the tag,
      cut a real GitHub Release via `gh release create` with notes
      pulled straight from the CHANGELOG entry, the same discipline
      already established for every release in this project. CI
      (`Buying brain tests`) confirmed green on the pushed commit via
      `gh run list` before calling this done, not just assumed from a
      clean local push.
  - SHORTLIST/WATCHLIST SYNC BUILT, THE ONE PLATFORM ACTION APPROVED SINCE
    2026-06-09 AND NEVER BUILT UNTIL NOW, 2026-08-27 (Mark: "would it be
    possible when we shortlist a car in bidbrain add that to watched /
    tracked in auction sites?" then "ok lets make that happen"). Real live
    reconnaissance first on both platforms, the same discipline as every
    other write this project has ever built, treated with real caution
    since this is the FIRST genuine platform write action anywhere in
    BidBrain's history (golden rule 1's one standing exception).
    - MOTORWAY, fully proven both directions on a real live auction car
      (HK65 MVP, vehicle 22660326): a real toggle on the car's own detail
      page, `label[class*="ShortlistStar_shortlistStar"]`, `aria-label`
      "Add to shortlist", `aria-checked` true/false. Clicked it on, closed
      the browser, opened a genuinely fresh session and reloaded the same
      page, confirmed `aria-checked="true"` persisted server side, not
      just a local DOM change; then reversed it and confirmed the same
      both ways. New `motorway.set_watchlist(page, vehicle_ref, watched)`
      builds on this directly, always verifying with a fresh reload
      before ever reporting success, same discipline as every DealerKit
      write in this project.
    - CARWOW, mechanism found and built, but its real behaviour on an
      ACTIVE listing is UNVERIFIED, recorded honestly rather than papered
      over. Carwow's own stock was genuinely empty both times it was
      checked (the daily 3:30pm to 5pm dead window this project has
      documented since 2026-06), so the only real listing reachable to
      test against was an already WON purchase (listing 14210244). Its
      real bookmark toggle (`<turbo-frame id="listing-bookmark-<id>">`, a
      real form POST to `/dealers/listings/listing_bookmarks/<id>/
      toggle`, confirmed via the real Stimulus controller's own source,
      `listing_bookmark_component_controller`, fetched and read directly
      rather than guessed) DID fire and DID get a genuine 204 success
      response, but a fresh reload afterwards kept showing the ORIGINAL
      state both times, in both directions. Most likely a won listing's
      own bookmark is simply locked server side once the car is bought,
      which would make this a non issue for real, still live listings,
      but that theory is UNPROVEN. `carwow.set_watchlist` was still built
      using the real, correct mechanism (never guessed), with the same
      always-verify-with-a-reload discipline as Motorway's own, so if the
      theory is wrong a real mismatch surfaces as a genuine, visible
      error rather than a silent false success. Needs a real check
      against an active Carwow listing once stock repopulates before
      fully trusting it in production; both function docstrings record
      this caveat directly.
    - Storage: new `watchlist_sync` table (`bidbrain/db.py`), keyed on
      (reg, platform), holding the DESIRED state, not an event log, so a
      quick star then unstar before a sync ever runs just updates the one
      row. `queue_watchlist_sync`/`pending_watchlist_syncs`/`mark_
      watchlist_synced`: a success clears the row, a failure leaves it
      pending with the real error recorded, so the next pass genuinely
      retries rather than silently giving up.
    - Orchestration: `daily_run.watchlist_sync_pass()` (`--watchlist-
      sync`), headless throughout (no login window needed, the star
      action itself already proves both sites are logged in), one
      continuous session per platform for its whole pending batch, same
      "never a fresh session per car" lesson already learned the hard way
      for Glass's and DealerKit. `serve.py`'s `_bid()` queues the desired
      state the instant a star is toggled then kicks this off as a
      background subprocess (`_kick_watchlist_sync`, best effort, never
      raises, so a busy moment just leaves the star saved locally with
      the platform sync catching up on the next trigger), sharing the one
      browser profile busy lock every other headed pass already uses. A
      new "Sync watchlist to platforms" button in the Run modal's Other
      checks section covers the on demand catch up case, same self
      contained poll shape as the CompMatch sync button right above it
      (its own `/api/run-status` flag, never routed through the shared
      cockpit progress bar, which is keyed to a different pass's own
      phase).
    - `cardData()`'s own platform value is the card's lowercase CSS class
      key ("motorway"/"carwow", from `_platform_class`), not the exact
      cased strings the purchases table and this new pass both key on, a
      real mismatch caught before it ever shipped by checking `_platform_
      class`'s own source rather than assuming; `_bid()` now maps it to
      the real canonical form. `data-listingurl` added to the shortlist
      card (`car.listing_url`) and `cardData()`, the one piece of
      information neither the star toggle nor the bids table had ever
      needed before this.
    - Verified live end to end through the REAL running app, not just the
      reader functions in isolation: a real POST to `/api/bid` (starring
      a real Motorway shortlisted car, YD16TYP) correctly queued the sync
      and, confirmed via `ps`, genuinely spawned the background `daily_run
      .py --watchlist-sync` subprocess automatically with no extra
      trigger; the pass ran, tried the real car, and correctly reported
      an honest failure once it found the car had gone "Vehicle no longer
      available" (the real auction had closed since the earlier
      reconnaissance, proving the error handling path works exactly as
      designed, not just the happy path) rather than a false success. The
      test star and its now moot pending row were both cleared afterward,
      leaving no test state behind. 10 new tests (a full `watchlist_sync`
      round trip: queuing, pending per platform, a confirmed sync
      clearing the row, a re queue overwriting a stale desired state to
      the latest one not stacking duplicates, a failed sync staying
      pending with the real error recorded so the next pass retries, and
      a car re queued after an earlier success genuinely re syncing
      rather than being skipped as already done). 592 tests pass.
    - Deliberately not built yet: no Settings > Integrations on/off
      toggle for this feature (every other automation in this project
      has one; this one currently has no way to switch off other than
      simply never starring a Motorway or Carwow car), and no cleanup of
      a pending row for a car that has permanently left the shortlist
      (it just keeps quietly retrying and failing on every future sync,
      cheap but not tidy). Both flagged here rather than built blind
      under an already large change.
  - A SECOND MOTORWAY ACCOUNT ADDED TO THE WATCHLIST SYNC, WITH ITS OWN
    AUTOMATED LOGIN, 2026-08-27 (Mark: "we have 2 accounts for MW as to
    make a purchase it has 2fa via sms and so can we add another account
    to push the watched cars from BB to both accounts this is
    accounts@rightdrive.co.uk, also make this a auto login in so its
    always active"). Real purchasing needs a person present for the SMS
    code that inbox receives, so this account never reads stock or bids,
    it only ever receives a copy of the same shortlist sync the primary
    account already gets, built minutes earlier the same day.
    - `bidbrain/browser.py`'s `open_context` gained a `profile_dir`
      parameter (defaults to the existing shared `PROFILE_DIR`), and a
      new `ACCOUNTS_PROFILE_DIR` constant (`data/browser_profile_accounts
      /`, gitignored alongside the primary one) gives this second account
      its own genuinely separate persistent Chrome profile, never the
      shared one every other Motorway read and write in this project
      already relies on, so the two logins can never clash or evict each
      other.
    - `daily_run.watchlist_sync_pass()` reworked to treat the second
      account as its own genuinely independent sync target, "Motorway
      (accounts)", not a second attempt folded into the primary
      "Motorway" row: `serve.py`'s `_bid()` now queues BOTH platform keys
      for the same star (skipping the accounts one when that connection
      is hidden on the Integrations tab), each getting its own real
      pending/synced/error row in the SAME `watchlist_sync` table, so a
      login not yet set up (or a transient failure) on one account can
      never block, or be blocked by, the other's own already successful
      sync, verified with 2 new db round trip tests. The per platform
      loop's own `site_key` parameter became an `opener` callable so the
      accounts variant can open its own separate profile directly
      (`browser.open_context(..., profile_dir=browser.ACCOUNTS_PROFILE_
      DIR)`) rather than routing through `open_reader_context`, which has
      no reason to know about a profile that is not the shared default.
    - New `auto_login_motorway_accounts.py`, closely modelled on the
      primary account's own `auto_login_motorway.py` (built earlier this
      session), with two real differences: it opens `ACCOUNTS_PROFILE_
      DIR` instead of the shared profile, and its own credential lives
      under a deliberately distinct keychain "server" string,
      `pro.motorway.co.uk.accounts` (a real macOS keychain server field is
      just a lookup key, not a resolvable hostname, and the real site is
      genuinely the same `pro.motorway.co.uk` for both accounts, so a
      literal duplicate string would have made the two logins ambiguous
      to look up). Also watches for a real one time code input appearing
      after the login form is submitted (Mark's own "2fa via sms") and
      fails loudly with a clear message pointing at the manual fallback
      rather than hang, the same safety net already built for Dealer
      Auction's own occasional SMS step; login itself was never actually
      seen to trigger one while building this, so the check is
      defensive, not yet proven necessary.
    - `setup_credential.py` gained a new site, `motorway_accounts`
      (keychain server `pro.motorway.co.uk.accounts`, matching the script
      above), with a friendlier prompt label ("Motorway
      (accounts@rightdrive.co.uk)") than the generic `.title()` fallback
      every other site gets. `login.py` widened with a small `_EXTRA_
      SITES` dict so `python3 login.py motorway_accounts manual` works as
      the manual fallback (reusing "motorway"'s own real login_url/
      base_url, since it is not a genuinely distinct site in `browser.
      SITES`, just a second login), opening `ACCOUNTS_PROFILE_DIR`
      instead of the shared profile and recognising the same logged in
      URL shape as the primary account.
    - `render.py`'s Integrations tab (`_SITE_LABELS`, `_CONN_ORDER`,
      `_SITE_AUTO`, `_SITE_ABOUT`, the client side `SITE_LABELS` used by
      the relogin confirm dialogs) all gained a real "Motorway (accounts)"
      row, "Auto, 06:00 daily" badge (satisfying "always active"), same
      time as the primary account's own scheduled login since the two
      profiles are genuinely independent and never need to avoid each
      other the way every other headed pass sharing the ONE profile
      does. `serve._RELOGIN_SITES` widened so the settings page's own
      manual "Log in to Motorway (accounts)" button works too, and
      `serve._apply_hide` (already fully generic over any site not in
      `PLATFORM_KEYS`/`_INTEGRATIONS`) needed no code change at all to
      let this connection be hidden and shown again cleanly, proven by 2
      new tests.
    - New `com.bidbrain.motorway-accounts-login.plist`, `~/Library/
      LaunchAgents`, fires at 06:00 daily (loaded and verified via
      `plutil -lint` and `launchctl print`, a real registered calendar
      interval), matching the primary account's own slot.
    - HONEST GAP, found while building this rather than assumed: NEITHER
      Motorway account's automated login is actually usable on this Mac
      yet. `security find-internet-password -s pro.motorway.co.uk`
      returned nothing at all, and no `com.bidbrain.motorway-login.plist`
      exists in `~/Library/LaunchAgents` either, despite CLAUDE.md's own
      much earlier history describing this exact automation as already
      built and scheduled; that work most likely happened on Steven's own
      Mac (this shared repo genuinely serves two dealerships, see the
      2026-08-27 diverged branch merge entry above), not this one. Ran
      the new script directly to confirm it fails the correct, honest
      way with nothing stored (`FAILED: No saved login found for
      pro.motorway.co.uk.accounts... Run: setup_credential.py motorway_
      accounts`), verified live on the real settings page (a genuine grey
      dot, "Not set up on this Mac yet.", the "Auto, 06:00 daily" badge
      still showing correctly since that only ever describes the
      schedule, not whether it can succeed yet). Two real, necessary
      human steps remain for this to actually go "always active" as
      asked, neither of which a session can do on its own: `python3
      setup_credential.py motorway_accounts` (the real accounts@
      rightdrive.co.uk password, typed at a hidden prompt) for the new
      account, and, separately, the SAME step for the primary `motorway`
      site if Mark wants that automated too, since it currently is not
      either. 597 tests pass.
  - THE SECOND MOTORWAY ACCOUNT'S LOGIN GOT WORKING, PLUS A REAL BLANK
    ERROR MESSAGE BUG FOUND AND FIXED ALONG THE WAY, 2026-08-27, same
    day, straight after the account itself was built (Mark: "done", then
    running the real login check came back "Your username or password
    were incorrect", his own guess "i think i might have copied the
    password in twice as it was hidden"). Investigated rather than
    guessed at a fix.
    - A REAL BUG, not the password: the auto login script's own error
      detection checked for a real failure message only AFTER an extra
      navigation to a logged-in-only check page, by which point Motorway
      had already redirected back to a FRESH sign in form with no error
      shown, since the original failed submit's own tooltip belonged to
      that earlier page load, not this one. Confirmed live: the real
      server side response (a genuine `.mw-feedback-tooltip-content`
      element, "Your username or password were incorrect. Please try
      again.") was captured in a direct diagnostic run, proving the
      credential really was rejected, but the script's own printed
      reason was always coming back blank. Fixed in both `auto_login_
      motorway_accounts.py` and the primary account's own `auto_login_
      motorway.py` (the identical bug, copied when this one was built
      from that one): the error check now runs immediately after
      submitting, before ever navigating away, so a genuine failure
      always shows its own real reason from now on.
    - The actual credential still failed twice more even after a careful
      single paste, real evidence the STORED password itself, not a
      paste mistake, was wrong; Mark then suspected it might be an
      outdated one and checked macOS's own saved Passwords (System
      Settings, likely a genuine, more current saved login than whatever
      he was typing from memory). A real Terminal window was opened for
      each attempt (`osascript` driving Terminal.app directly, activated
      and visible on his own screen) rather than any credential ever
      passing through this session, matching the standing rule that
      passwords are never entered or seen here, even on the user's own
      offer to just say it in chat, which was declined and explained.
    - Verified live end to end on the fourth real attempt: `python3
      setup_credential.py motorway_accounts` (Mark, in the real Terminal
      window) printed "Stored.", the real login check then printed
      "Logged in to Motorway (accounts@rightdrive.co.uk). Session
      saved." with no error, `db.get_site_health()` recorded a genuine
      `ok: True`, and the real live Settings > Integrations page
      correctly flipped to a green dot, "Working, logged in today. Logs
      itself back in daily at 06:00." A real end to end push through the
      actual `/api/bid` endpoint (starring YD16TYP, the same real
      Motorway shortlisted car used to prove the primary account's own
      mechanism earlier the same day) queued BOTH "Motorway" and
      "Motorway (accounts)" independently and correctly attempted both:
      this specific car has genuinely gone off sale since the earlier
      test (the daily dead window, confirmed several times today, an
      honest fact not a bug), so both accounts consistently reported the
      same real "vehicle no longer available" style failure rather than
      one succeeding and the other silently doing nothing, proving the
      new account's own session is genuinely reaching and browsing
      Motorway's real dealer area, not just appearing to log in. Test
      star and both now moot pending rows cleared afterward. 597 tests
      pass (the error message fix touches no logic this file's own
      suite covers, verified live only, matching this whole area's
      established pattern).
    - The primary Motorway account's own automated login still has no
      credential stored on this Mac, a real, separate, still open gap
      already flagged the same day this account was built; unaffected by
      today's fix either way, both scripts now share the identical,
      corrected error reporting.
  - AN "UNDER OFFER" SECTION ADDED TO THE MAIN NAV, THE FIRST ATTEMPT
    CAUGHT AND CORRECTED MID BUILD, 2026-08-27 (Mark: "add a under offer
    section to the main nav for cars that are won but bids not accepts
    this is for all auctions"). The first build wrongly assumed this
    meant the purchases table's own Motorway "Awaiting confirmation"
    status, and shipped a filter view on the Purchases page around that
    (own toggle button, deep link, `isUnderOffer()`). Mark then said
    plainly, live, "on MW got to bids and offers then under offer, we
    have 2 cars there currently" — a real, entirely separate Motorway
    screen, `/account/bids/under-offer`, showing bids currently accepted
    as leading but NOT YET a confirmed sale at all, genuinely earlier
    than anything the purchases table has ever held. The whole first
    attempt was cleanly reverted (`git checkout -- bidbrain/render.py`,
    nothing had been committed yet) and rebuilt around the real data,
    caught before it ever shipped to a release.
    - Real live reconnaissance across every platform before writing a
      line of the real version, matching this project's own standing
      discipline (never guess a value or build against guessed markup):
      Motorway's own `/account/bids/under-offer` PROVEN, a real 2 car
      example read and matched exactly against the live account (LR68
      GCU, BT16TYV, correct reg/name/max bid/date), later re read
      showing genuinely 1 car after the other resolved, confirming this
      is a live, changing picture, not a fixed list. Carwow's own
      equivalent, `/dealers/listings/filtered/offers?bids_and_offers_
      type=under_offer` (a real client side radio toggle, also reachable
      directly by URL), confirmed structurally live with the account's
      own real "Under offer 0" count and its own real empty state text
      ("You'll see vehicles here when your bid is waiting for the seller
      to respond or the reserve hasn't been met"), but genuinely zero
      real cars there at build time, so its own per card field parsing
      (reused from the same real listing-card-component the stock list
      already parses) has never been proven against a real populated
      example, honestly flagged in its own docstring. Auction4Cars has a
      real equivalent too (My Account > Awaiting Payment, found via its
      own real account menu), but a substantially different, payment
      instructions style page layout, no populated example either, and
      genuinely different enough markup from anything already parsed
      that guessing a reader for it was judged too risky, left as a real,
      documented gap rather than shipped unverified. Dealer Auction: no
      distinct screen found in a reasonable search of its own account
      area, also left as a gap, its own existing purchases table status
      field stays the only signal for that platform.
    - New `motorway.read_under_offer`/`_parse_under_offer_row` (Bids &
      offers > Under offer, the same real datagrid row shape `_parse_
      didnt_win_row` already established) and `carwow.read_under_offer`/
      `_parse_under_offer_card` (reusing `_card_blocks` unchanged, the
      same real listing-card-component the stock list parses). Neither
      touches the purchases table, both correctly independent of it.
    - New `under_offer` table (`bidbrain/db.py`, `record_under_offer`/
      `list_under_offer`): a live SNAPSHOT, not an accumulating history,
      `record_under_offer` replaces the WHOLE set for one platform on
      every read, so a car that has since been accepted, lost, or
      otherwise resolved simply is not in the next read and drops out on
      its own, no separate cleanup needed, proven with a dedicated test
      (2 real cars down to 1 on a genuine re read, matching what
      actually happened live building this).
    - `daily_run.under_offer_pass()` (`--under-offer`), headless
      throughout (no login window needed, the same already-authenticated
      sessions every other read in this project already uses), reads
      Motorway and Carwow only (matching the two platforms with a real,
      built reader), recording each platform's own health under a
      DELIBERATELY DISTINCT key (`motorway_underoffer`/
      `carwow_underoffer`, never the bare `motorway`/`carwow` the daily
      stock read's own health already uses), so a failure here can never
      be mistaken for, or silently overwrite, the real signal of whether
      the daily stock read itself is working.
    - `serve.py`: `/api/under-offer` (GET, reads the stored snapshot) and
      `/api/run-under-offer` (POST, on demand refresh), sharing the exact
      same busy lock and subprocess pattern as every other on demand pass
      here (`_under_offer_proc`, folded into `_any_proc_busy`/
      `_busy_reason`/`_stop_run`/`/api/run-status`). A "Check under
      offer" button in the Run modal's Other checks section mirrors the
      existing "Sync watchlist to platforms" button right above it.
    - `render.py`: a genuinely new, dedicated page, `under-offer.html`
      (`render_under_offer`), NOT a filtered view of Purchases this time,
      since the data itself is real and separate; a 4th real entry in the
      Live Auctions/Purchases/Lost page switcher (`_PAGE_SWITCHER_PAGES`,
      a new hourglass icon, `_HOURGLASS_SVG`), reachable from every page
      that already carries that switcher. Deliberately compact rather
      than a full clone of the much larger purchases page (no sortable
      columns, no popovers, no gallery, none of it needed here): reg,
      vehicle name with a link out to the real listing, platform (with
      its own real logo pill), max bid, and when it was noted, fetched
      live from the API on every load, same "never baked stale into a
      server render" convention as every other admin page here.
    - Verified live end to end through the real running app, not just
      the reader functions in isolation: a real `POST /api/run-under-
      offer` correctly spawned the background pass (confirmed via the
      real run status flag), which read Motorway's real account and
      stored exactly 1 real car (BT16TYV, matching a direct re read of
      the same live account moments later), the real page correctly
      showed it with a working "view the original listing" link to the
      real Motorway vehicle page, and the page switcher correctly
      highlighted Under Offer while leaving Purchases and the other two
      unhighlighted. 15 new tests (the two real per platform row/card
      parsers against real and synthetic examples, a full `under_offer`
      db round trip proving the live-snapshot replace behaviour and that
      one platform's own read never touches another's, and both new
      pages carrying the real live API fetch rather than anything baked
      in). 612 tests pass.
  - UNDER OFFER GAINED A SECOND, GENUINELY DIFFERENT SOURCE, 2026-08-27,
    same day, right after the feature above shipped (Mark, a real live
    example: "this is interesting as BL11HAM is showing as bid accepted
    but altho its different to BT16TYV as thats under offer , BL11HAM is
    showing in our purchases but the bid hasnt been accepted yet this
    example needs to be added to the logic , we know the bid has been
    accepted when the MW status on its portal changes to reviewing
    docuements"). Confirms the first attempt at this whole feature (see
    the entry above, built around the purchases table's own status field,
    reverted once real reconnaissance showed the Bids & offers screen was
    the real source) was not entirely wrong, only INCOMPLETE as a
    standalone idea: a car can be genuinely under offer on the platform
    itself (never yet a purchase record at all, the Bids & offers
    screen), OR it can be a car ALREADY won and sitting in the purchases
    table, just not yet formally accepted by the platform, a second,
    real, distinct case this page needs to show ADDED to the first, never
    instead of it.
    - New `serve._pending_acceptance_purchases()`: filters
      `db.list_purchases()` for a row whose own platform status has not
      yet reached the real accepted milestone, using Mark's own exact
      rule for Motorway (still "Awaiting confirmation" counts, anything
      from "Reviewing documents" onward is already accepted) and, by the
      same generic extension already designed once before but never
      shipped, Carwow rows not yet in a known-accepted status set
      (`quote_accepted`/`collection_arranged`/`collected`, Carwow's own
      real flow starts already accepted, so this only ever fires if some
      other, not yet observed status turns up, kept generic rather than
      hardcoded to "never happens"). Each match is normalised into the
      exact same shape `db.list_under_offer()` rows already carry (reg,
      name, platform, max_bid, noted_at, listing_url), with max_bid
      falling back from `winning_bid` to the plain `price` field for a
      purchase whose own payment breakdown has not been read yet, and a
      new `stage` field, "Won, awaiting confirmation", so the two sources
      never read as one undifferentiated list.
    - `/api/under-offer` now returns both: `db.list_under_offer()`'s own
      rows tagged `stage: "Under offer"`, concatenated with
      `_pending_acceptance_purchases()`'s own already-tagged rows, never
      one replacing the other. `render_under_offer()`'s table gained a
      real Stage column (a small coloured pill, amber for "Under offer",
      blue for "Won, awaiting confirmation", `.stage-offer`/
      `.stage-pending`), so the two real sources read distinctly on the
      page itself, not just in the API response.
    - Verified live end to end against the real running server, not just
      the unit tests: `/api/under-offer` correctly returned BOTH real
      cars together, BT16TYV (genuinely still under offer on Motorway's
      own Bids & offers screen, tagged "Under offer") and BL11HAM (a
      real purchase, status "Awaiting confirmation" on file, tagged "Won,
      awaiting confirmation", max_bid correctly falling back to its own
      real £6,983 price since no winning_bid has been read yet), neither
      duplicated, neither replacing the other; the live `under-offer.html`
      page confirmed serving the new Stage column. 5 new tests
      (`_pending_acceptance_purchases` against a synthetic monkeypatched
      `db.list_purchases`, proving only the genuine "still Awaiting
      confirmation" Motorway row counts, a row already past that point
      does not, Carwow's own already-accepted first status does not, the
      max_bid fallback works, and the row carries its own distinct
      stage), deliberately NOT asserted against the real live database
      inside the permanent suite (both real examples will each resolve
      in time, and this file must not depend on a moving real business
      record, the same discipline already on record elsewhere in this
      project for exactly this reason). 617 tests pass.

  - THE AUC STATUS TRACKER'S OWN BID STAGE WAS SHOWING "ACCEPTED" WHEN IT
    WAS NOT, FOUND AND FIXED THE SAME DAY, 2026-08-27, straight after the
    Under Offer fix above (Mark: "thats wrong BL11HAM, is in the puchases
    section on MW and so that makes sense to mirror that but its also
    showing as bid accepted on BB on the purchases status section which
    is wrong"). The 3 stage progress tracker built 2026-08-26 (Bid /
    Documents / Delivery on the purchases page's own Auc Status popover)
    deliberately hardcoded Bid to always show done, on the reasoning
    "every row here already represents a won purchase, the fixed
    starting point of the tracker", confirmed with Mark via AskUserQuestion
    at the time. BL11HAM is the real counter example that reasoning
    missed: it genuinely sits in the purchases table (mirrors Motorway's
    own Complete purchases screen correctly, nothing wrong there), but
    its own real Motorway status is still "Awaiting confirmation", the
    exact same not-yet-accepted state the Under Offer page's new second
    source (_pending_acceptance_purchases, built minutes earlier the
    same day) already treats as genuinely not accepted. The tracker's
    hardcoded `true` was flatly wrong for this row.
    - `auBidDone` (bidbrain/render.py, inside the purchases page's own
      per-row JS): `r.platform === 'Motorway' ? (auLower !== 'awaiting
      confirmation') : true`, the exact same rule as
      `serve._pending_acceptance_purchases`, so the two pages can never
      disagree again. Carwow and every other platform still default to
      done, matching the same "no real counter example ever seen there,
      so leave it as it was" reasoning already applied to the Documents
      and Delivery stages for platforms with no finer signal.
    - The Bid badge (`auStage(auPoundGlyph, auBidDone)`, was `true`) and
      its own popover row (`auStageNote(auBidDone, 'Bid Accepted',
      'Awaiting confirmation')`, was a fixed string) both now read live
      off the real status, coloured orange (not yet) or green (accepted)
      the same way Documents and Delivery already did.
    - Verified live end to end against the real running server (restarted
      com.bidbrain.serve): BL11HAM's own real popover now reads "Bid,
      Awaiting confirmation" with an orange badge (#d39e00); KF66GJZ (a
      real Motorway purchase already past that stage) still reads "Bid
      Accepted" with a green badge, proving the normal case is untouched;
      GU68XPP (a real Carwow purchase) also still reads green, confirming
      Carwow's own always-accepted default was not disturbed. 617 tests
      pass (this is a pure JS display fix inside an already-generated
      page, no gate or pricing logic touched, no dedicated new tests,
      matching this whole page's own established pattern of live
      verification for a display-only change).

  - UNDER OFFER'S SECOND SOURCE REVERTED, MARK CAUGHT IT DID NOT MATCH
    THE REAL MOTORWAY PORTAL, 2026-08-27, minutes after it shipped (Mark:
    "BL11HAM need to be removed from the under offer section as its not
    matching the mw portal"). The earlier same-day change (merging
    `_pending_acceptance_purchases()`, purchases still at Motorway's
    "Awaiting confirmation", into `/api/under-offer` alongside the real
    Bids & offers snapshot) was itself wrong: Motorway's own real Bids &
    Offers > Under Offer screen does not show BL11HAM at all, it is a
    genuinely different real screen with a genuinely different real
    meaning (a bid not yet even resulting in a won purchase), and BL11HAM
    is already a won purchase sitting on Motorway's own Complete
    purchases screen instead. "Not yet accepted" was a real, useful fact,
    just never a fact that belongs on THIS page, matching what the
    platform itself actually shows.
    - Fully reverted: `serve._pending_acceptance_purchases()` removed
      outright (not left as dead code), `/api/under-offer` back to
      returning `db.list_under_offer()` alone, `render_under_offer()`'s
      Stage column and its `.stage`/`.stage-offer`/`.stage-pending` CSS
      removed (with only one real source left, a stage column had
      nothing left to distinguish), and the 5 tests built around the
      reverted function removed with it, back to 612 passing.
    - What was NOT reverted, and is still correct: the purchases page's
      own Auc Status tracker fix (its Bid stage now honestly reads
      "Awaiting confirmation" rather than a hardcoded "Bid Accepted" for
      a Motorway car still at that real status) stays exactly as built,
      that is the purchases page's own status, a genuinely different
      question from what belongs on the Under Offer page, and Mark's
      complaint here was specifically and only about the Under Offer
      page showing a car that does not match Motorway's own real Under
      Offer screen. The stale comment above `auBidDone` referencing the
      now-removed Under Offer merge was corrected to say plainly it is
      scoped to this page alone.
    - Verified live end to end against the real running server: `/api/
      under-offer` now returns only BT16TYV, the one genuine Motorway
      Bids & Offers entry, no BL11HAM; the rendered page carries no Stage
      column. 612 tests pass.

  - A FRESHLY WON MOTORWAY CAR COULD SILENTLY DROP OFF THE PURCHASES PAGE
    OWN DEFAULT VIEW, FOUND AND FIXED, 2026-08-27 (Mark: "we have won LR68
    GCU on MW i have refreshed the sync but its not pulled this through").
    Checked the real database directly rather than assumed a sync
    failure: LR68GCU had genuinely already synced in full, real winning
    bid, fees, VAT, photos, service history and V5 all present, but
    `bought_date` was stored blank. Found the real cause live, reading
    Motorway's own purchases screen text directly for this exact row:
    "LR68 GCU\n...\n£10,477\n.55\n57 mins ago\n...", Motorway
    abbreviates minutes as "mins" for a car won within the last hour,
    and `motorway._WON_AGO_RE` had only ever matched hour/day/week/month
    units, never minutes, since it was first written. So `_parse_won_date`
    correctly returned "" rather than guess (playing safe, per golden
    rule 4), but that meant `bought_date` genuinely never got filled in
    for a car this recently won, and the purchases page's own default
    date filter ("Bought on or after 1 June", added 2026-08-24) deliberately
    never matches a row with no `bought_date` at all, so the row was
    correctly synced yet invisible under the page's own default view.
    - Fixed by widening `_WON_AGO_RE` to also match "min"/"mins"/
      "minute"/"minutes ago" (treated as today, the same 0-days-per-unit
      treatment "hour" already gets), verified against the real captured
      row text. This is the one shared regex and parse function behind
      three real readers (`_parse_purchase_row` for the purchases screen,
      `_parse_didnt_win_row` for Didn't win, `_parse_under_offer_row` for
      Under offer), so all three benefit from the same fix, not just the
      one that was reported.
    - Verified live: re-parsed the real captured row text for LR68GCU
      through the fixed `_parse_purchase_row`, correctly produced
      `bought_date: '2026-08-27'`; ran a real `python3 purchases_run.py
      --motorway` re-sync to backfill the real date onto the already
      stored row (`bought_date` fills in once, never overwritten once
      set, so a blank one from before this fix correctly gets filled by
      a fresh read now that the parser can read it). 3 new tests ("57
      mins ago", "1 min ago" singular, "30 minutes ago" spelled out, all
      resolving to today). 615 tests pass.


