# BidBrain project status, part 20: v3.10, top picks, one run model, Dealer OS cockpit (2 September 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.10 starts here, 2026-09-02 (after the v3.9 tag was cut).**
  - THE REVIEW'S STEPS 3, 4 AND 5 BUILT, 2026-09-02 (Steven: "use /design
    to mockup the changes properly", then, on the seven artboard canvas at
    https://claude.ai/code/artifact/920d46a6-671f-4c63-9f5f-127f673e914f,
    "ok build step 3 4 and 5"). Every screen was matched to render.py's
    own tokens and components before drawing, and built from the same.
    - STEP 3, TOP PICKS: daily_run._pick_score (pure, per priced car:
      room as a share of reserve weighted most, stock gap, taste match,
      CompMatch's suggestion and whether it sits inside the max bid, new
      since yesterday, a mark down for no service history) and
      _rank_picks (top TOP_PICKS = 25 get car._pick_rank), both run at
      the end of _write_pages so a real run and --render-only agree.
      car._is_new comes from db.repeat_summary (no earlier sighting in 30
      days). On the page: a "filterpicksonly" checkbox in the Filters
      panel, ON by default (the first filter with a checked default,
      _filter_check gained a checked= parameter), stored as '1'/'0' in
      criteria because '' means "use the default" in viewVal and the
      default is on; VIEW_DEFAULTS picksonly:'1' so activeViewName reads
      "Top picks" on the untouched page and "All cars" with only that
      filter off; the views chip (picks=True on the cockpit only, the
      other pages' chip is untouched, a test pins it) gained an All cars
      row, and a "Show all N" link beside the chip (showAllCars). With
      picks on and the default sort, cards order by rank; a new "newest"
      sort (New since yesterday) orders new cars first, each group by
      pick score. Rank and New today badges on the card, data-pickrank /
      data-pickscore / data-isnew attributes.
    - STEP 4, HIDE THE PLUMBING: the Run modal's "Other checks" grid is
      folded under a Maintenance header (rmmaint-head, toggleMaint,
      remembered in localStorage bb_maint_open, forced open by
      paintMaintSummary whenever a check is failing, its one line summary
      naming the failing checks or "all fine · last Xh ago"); every
      button keeps its id and handler. Settings' top bar went from eight
      tabs to four (Buying rules = data-view overview, Purchases,
      Connections = integrations, Look = theme) with the five platforms
      as #rules-tabs INSIDE Buying rules; switchView keeps its
      fine grained view (overview/platform/purchases/theme/integrations)
      and paints the top tab from it, so every ?tab= deep link (the
      bell's ?tab=integrations, a platform name) lands as before. Two
      bugs caught live: the deep link reader only looked in #top-tabs
      (fixed to both), and the sub tabs still showed on Look because
      .tabs' display:inline-flex outranks [hidden] (style.display now).
    - STEP 5, THE CAR RECORD DRAWER: render._drawer_data(a, rows) packs
      each card's facts, pricing receipt (spread = governing minus max
      bid), flags (the same _insight_rows the Details pop-up drew),
      CompMatch (bid_suggest.suggest_bid_for_car now also returns
      "comps", the top 8 comps actually used, name/amount/retail/
      mileage/year/transmission/date, never a reg) and history
      (db.repeat_summary, db.reserve_history, learned_for, set on the
      car as _history in _write_pages) into a data-drawer JSON attribute
      on the card. Details opens openCarDrawer; the drawer (cd- prefixed
      CSS, the purchases drawer's own sizes, 940px, record left, tabs
      right, footer with View on platform / Star / Hide) is painted
      entirely client side from that JSON, Escape closes it first in the
      ladder, its star drives the card's own starbtn via toggleBid, Hide
      opens the card's own hidebox, photos open the card's own gallery.
      The old Details pop-up markup stays in the card (hidden), unused.
      One escaping bug caught by node --check before it ever shipped:
      \' inside the render_page f-string collapses to a bare quote, use
      &#39; in generated HTML attribute strings.
    - PROVEN live on the real cockpit: 25 cards open by default in rank
      order, "Show all 115" widens to every car and the chip reads All
      cars, New since yesterday puts isnew cars first, Reset returns to
      Top picks; the drawer opened on a real Ecosport with the real
      receipt (£9,683 Cazana, +15% £11,135, less £3,000, max bid £8,135),
      5 real comps, History and Flags, and closed on Escape; the
      Maintenance row read "9 checks · all fine · last 2h ago" collapsed
      and opened on click; settings.html?tab=carwow lands on Carwow
      inside Buying rules, Look hides the picker. 1,059 tests pass (11
      new). Not committed, Steven's call; Step 6 (Purchases on the
      phone) untouched.
    - DETAILS STYLE, A PER DEALERSHIP CHOICE, same day (Steven: "i want
      the ability in settings to select whether to show the drawer when
      clicking details on a car card or show the pop up modal that it
      just replaced. mark might prefer it one way and me another"). New
      pricing.DEFAULT_SETTINGS["details_style"] ("drawer" default,
      "popup" the old floating panel, which every card still renders for
      exactly this), validated in serve._validate_settings (missing
      reads as the default so older payloads pass, anything else
      refused), passed by _write_pages into render_page(details_style=)
      and baked as DETAILS_STYLE in the page JS; the card's Details
      button calls openDetails(this), which dispatches to openCarDrawer
      or the old toggleDetails. A "Car details" card at the top of
      Settings > Look holds the select. Proven live end to end: saved
      popup through the real page, rebuilt the cockpit, Details opened
      the pop-up not the drawer, restored drawer. 1,062 tests pass.
    - TOP PICKS SHOWN AS ONE LIST UNDER ALL, same day (Steven: "why am i
      missing top pick 1 2 and 3?"). They were on the page: all three were
      Carwow cars, and the All view lists cars in per platform sections
      with Motorway first, so #4 to #9 sat at the top and #1 to #3 were
      about 7,000px down under the Carwow heading. _platform_sections now
      emits an empty "Top picks" section first, and a new arrangeSections()
      (called at the top of applyView) moves every card into it while Top
      picks is on under All, and back to its home section (data-home,
      recorded on the first pass) otherwise, so a platform tab, Show all
      and Reset all still read as before. Proven live: All reads #1 to #8
      in order in one section, Carwow alone reads 1, 2, 3, 10, 12..., Show
      all restores the Motorway 80 / Carwow 33 sections. 1,062 tests pass.
    - SOLD HISTORY AND MARGIN MADE THE STRONGEST PICK SIGNALS, same day
      (Steven: "what about cars i have bought before, i tend to buy the
      same cars for a reason, this should be a very strong signal, as
      should margin achieved after prep"). _pick_score gained a sold
      parameter (this model's db.learned_for record, 87 models on
      Steven's Mac Studio from 530 real sold cars, June 2025 to May
      2026): bought before is now worth up to 4 points (1.5 + 0.5 per
      car sold, capped), average margin after prep up to 3 (3 at £2,000,
      a loss goes negative, floored at -2), and a model that sells in 30
      days or under earns 0.5; a model never sold scores exactly as before,
      not penalised. _write_pages passes the record keyed by
      clickdealer.model_key. Rebuilt live: the top 10 reshuffled to Steven's
      real repeat buys (Tucson sold 9, Peugeot 3008 sold 8, Fiesta sold 7, Focus sold 21). 4 new tests,
      1,066 pass.
    - FORD 1.1 PETROL BANNED LIKE THE 1.0, same day (Steven: "i want 1.1
      petrol fords to be treted the same as 1.0 litre fords"). Same 2014
      to 2019 window as the Ford 1.0 EcoBoost, in both places that ban
      lives: BANNED_ENGINES ("Ford 1.1 petrol, 2014 to 2019", keyed on the
      generic fuel rule key since the 1.1 carries no EcoBoost badge) and
      LITRE_BANS ("Ford 1.1 petrol", for the CSV shape with only litres
      and a fuel field). An unknown year behaves exactly as it does for
      the 1.0 (the rule is skipped), pinned by a test comparing the two
      rather than asserting either. --reprice on the saved run rejected 4
      real Fiestas (2017 to 2019), three of which had been sitting in the
      day's Top picks. 6 new tests, 1,072 pass.
  - ONE RUN MODEL, 2026-09-02 (Steven: "check the way all of the runs are
    set up, how the runs are displayed throughout the app... it all seems
    sporadic, sometimes runs work, sometimes they dont, sometimes the icon
    shows things its not supposed to, the notifications that pop up arent
    the same design... the maintenance checks dont always act like the
    auction house runs... some dont even use the progress bar when they
    should"). Audited first with three parallel agents (server side,
    client side, and the real logs on this Mac Studio), findings in the
    session's plan; then built in four parts, the plan Steven approved:
    - bidbrain/runs.py, THE REGISTRY: one Run per thing that can run
      (key = the db.run_status key, label, argv, busy sentence, starting
      line, site, gate, headed, scope bulk|row|login, win, confirm text),
      plus progress()/finish()/guard()/RunSkipped. serve.py's 14 proc
      globals, _STOPPED_RUN_KEYS, _RUN_KEY_SITE, _RUN_KEY_INTEGRATION and
      _row_procs are gone; _procs is one dict keyed by registry key, the
      16 near identical spawn handlers are one _start_run (gate check,
      busy check, prime, spawn with the registry's argv) behind a new
      POST /api/start-run {key, ...} with the old endpoints kept as thin
      aliases; _stop_run records a stop for every bulk key it kills;
      _run_status exposes one <key>_running flag per registry entry (the
      old drifted names compshare_running/dealer_os_running/
      dealerkit_running/lecapital_running are gone, plain "running" kept
      for the daily run). Row jobs (dealerkit_push_row, dealerkit_check_
      row, purchase_check) have their own slots, so Stop and the light
      can never blame the bulk pass for one car's job.
    - ONE FINISH: daily_run._progress/_finish are runs.progress/finish;
      purchases_run._finish_sync, lost_bids_run and share_comps all call
      runs.finish; every script entry point runs through runs.guard
      (normal return, RunSkipped, any exception, SystemExit, Ctrl-C all
      land in a terminal write; a row scoped key writes progress only).
      "skipped" is a real phase: every off-in-Settings path raises
      RunSkipped (grey toast, ok record prefixed "Skipped."), never an
      error. purchases_run's per row sys.exit(1)s became raises; its
      exit(1)-after-done is gone; share_comps' blank DEALER_NAME is a
      skip not a crash. serve._bid only queues "Motorway (accounts)" when
      that account's own auto login has recorded ok on this Mac (Steven's
      copy had 16 stuck rows and a permanent red light for Mark's second
      account, cleared). com.bidbrain.dealerkit-login moved 17:00 -> 15:30
      so it runs before the 15:50 purchases sync (a real gap in the logs).
    - ONE DISPLAY KIT (render.RUN_KIT_CSS/RUN_KIT_JS_TEMPLATE, spliced by
      _frame_css/_frame_js and the settings page): toast(msg, ok) (green
      true, red false, plain otherwise; flash() is an alias so every old
      call lands on it), one #runbar markup (_runbar_markup) on all five
      pages, renderProgress (the cockpit's, generalised, dead run cutoff
      kept), runLock on [data-run=key] buttons, startRun(key, body, opts)
      (asks the registry's confirm unless opts.confirm is '' because the
      page's own modal was the question, posts /api/start-run, locks,
      shows the bar, nudges the light, watches), watchRun (3s, 5s on
      error, toast from the terminal phase), resumeRuns on load, stopRun,
      and the bell (toggleBell/loadNotices) once instead of five copies.
      Deleted: purchases' own bar CSS and pollProgress plus five wrappers,
      the lost bids/under offer 3s loops and #sync-status, the cockpit's
      pollRun/renderProgress/four maintenance pollers/rmCardBusy/Done/
      flash, settings' flash, 38 alert() calls. The cockpit keeps
      setRunning and gains onRunTick (a page hook the kit calls each tick)
      and reloadIfDone for the passes that rewrite the page; the modal
      repaints every ~9s while open during a run. A red light click opens
      the run board on every page (SYNC_LIGHT_JS capture listener). Off
      buttons carry data-run-off so unlock never re-enables them.
    - PROVEN LIVE on this Mac Studio: an Under offer check started from
      its own page showed the bar and the green "0 under offer" toast on
      the Settings page too; --glass (off here) wrote phase skipped and an
      ok "Skipped." record; a planted CompMatch failure turned the cockpit
      light red, clicking it opened the board with the failing card, a run
      from the Maintenance card locked the card ("Running..."), the Run
      button and showed the bar, and Stop mid run hid the bar, toasted and
      recorded "stopped". 1,082 tests pass (10 new: registry shape, guard's
      four exits, row scope, the kit present and alert() absent on all
      five pages). DEALER OS / EXTRA: Steven's Mac shows "Update available
      v3.9.1", a release cut on Mark's side, not applied here (this tree is
      uncommitted). Not committed, Steven's call.
    - Toasts moved from the foot of the screen to the run bar's own level
      just under the nav bar, on the eye line (Steven, same day: "i dont
      like the toast messages being at the bottom of the screen"); toast()
      measures .controls so the top is right on every page.
    - THE RUN MODAL ON EVERY PAGE, same day (Steven: "when i click on the
      run button on any page other than live auctions the modal doesnt pop
      up, instead it kicks me back to the actual live auctions page?", the
      red light click was deep linking to cockpit.html?openrun=1; asked,
      he chose "Same Run modal everywhere" over keeping the pickers). The
      cockpit's modal CSS, JS and markup were lifted out into shared
      RUN_MODAL_CSS / RUN_MODAL_JS (spliced by _frame_css/_frame_js and the
      settings page) and _run_modal_markup(platform_enabled, hidden_
      connections, integrations) (a {{RUN_MODAL}} token on the four plain
      pages, rendered with defaults since openRunModal re-syncs live from
      /api/settings on open). The three status-only link cards became real
      buttons (Purchases sync, Didn't win sync, a new Didn't win full
      history with its own confirm, DealerKit records, gated by the
      dealerkit_purchases toggle); the purchases and lost bids sync modals
      and their CSS/JS are deleted; every page's light button calls
      openRunModal(); settings' light is a real button now. runCheck(key,
      body, confirmText) -> runFinished(key, s): reload on the cockpit for
      the passes that rewrite it (window.RUN_PAGE_RELOAD), refreshRunSummary
      elsewhere, then the page's own onRunFinish(key, s) hook (purchases,
      lost bids, under offer refresh their table). resumeRuns and the
      ?openrun=1 reader live in the modal JS, once. The toast anchors under
      the lowest real sticky row (.controls / .platform-toggle-row measured
      by computed position, not by class name; a first version anchored to
      the wrong element on Settings). Proven live: the modal opens in place
      on all five pages with the same rows and 10 cards. 1,082 tests pass.
    - WATCHLIST SYNC GIVES UP ON LOST CAUSES, same day (Steven: "yes do
      that", after two Carwow cars that would never bookmark kept every
      star click re-running the sync and the bar busy). watchlist_sync
      gained an attempts column (idempotent migrate, reset to 0 by
      queue_watchlist_sync on a fresh star or unstar, incremented by
      mark_watchlist_synced on a failure); pending_watchlist_syncs skips
      rows at or past db.WATCHLIST_MAX_ATTEMPTS (3), their real error kept
      on file; new db.prune_watchlist_sync(active_regs) drops still
      pending rows for a car no longer in the last saved run's shortlist
      or held (never a synced row, that is the record), called at the top
      of daily_run.watchlist_sync_pass. Applied live to Steven's own
      database. 5 new tests, 1,087 pass.
    - THE RUN MODAL REDESIGNED AS TWO TABS OF MATCHING ROWS, same day
      (Steven: "sometimes it takes a long time for the sync modal to
      appear and sometimes i have to click it 3-4 times" and "have the
      maintenance checks change from cards to look the same way as the
      auction runs do", then, on the first one-list mockup, "a lot of
      info at once... too much space on a small screen... auction ones
      open... on the auction screen and vice versa on every other
      screen"; three directions mocked on the design canvas, he chose
      B, tabs). (1) openRunModal no longer awaits /api/settings before
      showing: that read can sit behind a database lock for up to five
      seconds while a run writes, so the click looked dead; it opens at
      once and the two fetches fill it in. (2) render._RUN_MODAL_CHECKS
      (key, label, site, gate, extra body) drives Maintenance rows with
      the exact platform row anatomy (.rmrow.rmchk: tick .rm-check,
      name, .rmstate dot, .rmcars result with ellipsis and a full title,
      .rmage, .rmwhy with the reason and a Log in button only where the
      site can be relogged in); the card grid, rmmaint head and every
      per card run* function are gone. Two tabs (.rmtabs/.rmtab,
      rmSetTab) with a live badge each (count, or "N need attention" in
      red); data-default-tab on the overlay, "auction" from render_page
      and "checks" on every other page, reapplied on every open. States:
      Worked, Failed (row tinted, floats to the top), Skipped (a run_
      status summary starting "Skipped."), Stopped (the _STOP_SUMMARY
      sentence, grey, not a failure), Never run, Off in Settings (row
      .off, tick disabled, sinks to the bottom); hidden connections not
      rendered. (3) One Run for everything: confirmRun builds rmQueue
      (the ticked auction houses as one daily run, then each ticked
      check with its own data-body, e.g. lost_bids backfill), rmNext
      runs them one at a time through startRun with confirm '' (the
      modal was the question), reloading the cockpit once at the end if
      any page rewriting pass finished cleanly; rmNote paints the footer
      sentence from the ticks ("Runs the 3 ticked auction houses, then
      DealerKit stock. One at a time, so nothing clashes."), busy or
      stale text outranking it. Checks start unticked, auction houses
      ticked. A live bug caught on the first look: the platform row
      selector .rmrow[data-site] also matched the four check rows that
      carry a site, pre ticking them and counting them in the auction
      badge; both selectors now exclude .rmchk. Modal widened 660 to 680
      and the grid to 1fr 96px 176px 58px so a result reads. Tests
      updated (the Dealer OS off state now on the row's tick, the two
      structural row classes allowed in the orphan scan). 1,088 pass.
    - Select all / deselect all per tab, same day (Steven): a tick in each
      table header's first cell (.rmall/.rm-all, rmToggleAll) ticks or
      clears every enabled row on that tab; rmPaintAll (called from rmNote)
      keeps it checked, clear or part ticked (indeterminate) as the rows
      change. Proven live on the cockpit: clearing the auction tick empties
      the footer sentence, ticking all checks lists all ten, unticking one
      turns the header tick part ticked.
    - Default ticks follow the page, same day (Steven): rmDefaultTicks on
      every open (and again after the live settings sync, which re ticks
      the platform rows itself): on Live Auctions every auction house
      ticked and no check; on every other page every check ticked and no
      auction house. Proven live: cockpit 3 / 0, Didn't win 0 / 10 with
      the header ticks and footer sentence agreeing.
    - DEALERKIT PASSES LOG THEMSELVES IN, same day (Steven, on seeing
      "Waiting for you to log into DealerKit (286s left)" on the bar: "i
      thought dealerkit was supposed to login using the saved password
      thing we have set up? i want to be able to sync from any device...
      i dont want to have to remotely access the host mac to login"). The
      three DealerKit passes (dealerkit_pass, dealerkit_purchases_pass,
      dealerkit_check_pass) each opened a HEADED window and waited up to
      300s for a person whenever the saved dealerkit_state.json had
      expired, never once trying auto_login_dealerkit.py (the keychain
      credential Steven set up 2026-08-26). New daily_run._open_dealerkit
      (p, url, marker) -> (ctx, page, logged_in): tries the saved session
      headless, on failure runs auto_login_dealerkit.py as a subprocess
      (which rewrites the state file) and tries again, and only if that
      fails too falls back to the old headed wait, with the reason on the
      bar ("Could not log into DealerKit by itself (...). Waiting for you
      to log in on the Mac"). All three passes replaced their own copy of
      the wait loop with one call. Proven live through the real app: with
      a genuinely expired session the stock check went "Opening DealerKit
      with the saved login..." -> "The saved DealerKit session has
      expired, logging in again with the saved password..." -> "45
      vehicles in stock" in 21 seconds, no window, light back to green.
      LE Capital's pass still has the old headed wait (it drives LE
      Capital and DealerKit in one persistent profile window); worth the
      same treatment with auto_login_lecapital.py next, not done here.
    - LE CAPITAL GIVEN THE SAME, same day (Steven: "do the same for that
      but i cant test it as i dont have access to it"). The helper became
      the general _open_logged_in(p, site, url, ready, script, label)
      (ready(page) decides logged in; _open_dealerkit and a new
      _open_lecapital wrap it). lecapital_funding_pass no longer opens one
      headed persistent profile window for both sites: it opens LE
      Capital from its own saved session (auto_login_lecapital.py on
      expiry), reads Current Stock and Stock History, closes, then opens
      DealerKit the same way for the funding work; the two old wait loops
      are gone. NOT proven live (LE Capital is hidden and unused on this
      Mac; Mark's copy is the real test): the pass compiles, skips cleanly
      here, and the helper's control flow is unit tested with a fake
      browser (a live session used as is; an expired one runs the script
      once then reopens headless, with the plain worded bar message).
    - ONE MORE CARD PER ROW, same day (Steven: "on my macbook air 14 inch
      ... it only displays 3 cars in a row and should display 4. make it
      so on each screen size an extra car can fit in"). The cockpit grid's
      per card minimum went 360px to 290px and the gap 20px to 16px
      (render.py .grid, the one rule, auto-fill does the rest). Measured
      live in a real browser: 1280px 3 to 4, 1470px 3 to 4, 1920px 4 to
      6, 3440px 8 to 11. A real collision caught in the 1280px screenshot
      (the three price cells overlapping at 294px) fixed with a container
      query on .hf-card (container-type:inline-size; under 330px the Max
      bid cell takes its own line above Reserve and Retail, the two big
      figures drop to 17px, the CompMatch line wraps), so the narrow card
      reads cleanly at every grid size, not just the window it was
      measured at.
    - THEN SCALED, NOT SQUASHED, same day (Steven: "you have made the
      cards fit but kept all the elements on the cards the same size so
      now everything is too squashed"). New paintCardScale (called at the
      end of applyView and on resize) reads each grid's resolved column
      width straight from grid-template-columns and sets --card-zoom =
      min(1, column / 360) on the grid; .grid .card {{ zoom: var(--card-
      zoom, 1) }} then lays every card out as if it were 360px wide and
      draws it at the column's width, so chips, figures, gauge and buttons
      all shrink in step rather than one line colliding. Measured live:
      1280px zoom .817 (card laid out at 357 css px, drawn at 294), 1470px
      .949, 1920px .826 six across, rows filling their grid exactly. The
      container query stacking from the entry above is superseded and
      removed (a scaled card never gets that narrow inside).
    - ALL CARS IS THE DEFAULT VIEW, AND THE TOP PICKS ROW COUNTS ITSELF,
      same day (Steven, from the views menu: "top picks shows 109 cars
      here? also all cars should be the default view"). The Top picks row
      called viewCount({}) (no picks filter, so the full 109); it now
      counts viewCount({picksonly:'1'}). The default flipped: the Top
      picks filter box starts unticked, VIEW_DEFAULTS picksonly '0',
      resetFilters clears it, activeViewName reads the untouched page as
      "All cars" and picks-on as "Top picks", the views chip's first row
      is All cars (resetFilters) with Top picks (showTopPicks, replacing
      showAllCars) beneath it, and the link beside the chip reads "Top
      picks N" while the filter is off instead of "Show all N" while it
      was on. Proven live: the page opens on All cars 109 with the Top
      picks row reading 25, the link switches to Top picks 25, Reset
      returns to All cars 109.
    - COLUMN LAYOUT IS PART OF A PURCHASES VIEW, same day (Steven: "when
      resizing a column or dragging and dropping one and changing the
      order happens in the purchases screen its supposed to show as a
      custom view that can be saved and stacked on top of in the same way
      changing a filter does"). Column order, widths and which columns
      show used to write straight to the dealership wide purchase_columns
      on every drag and never touched the view at all. Now COLS_BASE is
      the saved default layout and COLS_CFG the live one; colsSnapshot()
      turns a layout into one canonical string; currentCriteria() carries
      it as criteria.columns ('' when it equals the default, so a view with
      no layout of its own means the default); samePView compares it, so a
      resize, reorder or Fields change turns the chip Custom exactly like
      a filter; saveCurrentView keeps it; applyPCriteria restores the
      view's own layout or the default; resetFilters (All cars) restores
      the default. Making a layout the default for every device is now an
      explicit "Make this the default layout" button in the Fields menu
      (shown only while the live layout differs), instead of every drag
      silently rewriting it. serve._validate_purchase_views keeps a
      criteria.columns string when it parses as an object (capped at 6000
      chars, well above a full layout). 2 new tests, 1,093 pass.
    - THEN SWITCHED TO THE AIRTABLE MODEL, same day (Steven: "when you
      change one of those elements it highlights that its a custom view
      but there is no way to reset it like you can do with the filters?
      ... do we just handle this the same way airtable does and it doesnt
      highlight a custom view but just auto saves the layout on that view
      forever until you change it?"). Yes: the layout now belongs to the
      view you are on and saves to it as you change it, never turning the
      chip Custom. activeBaseView remembers which view that is (a saved
      view's name once applied or saved under, null for All cars);
      saveColsCfg writes the snapshot into that view's criteria.columns
      and savePViews, or, on All cars, to purchase_columns as the default
      layout (COLS_BASE updated in step). samePView no longer compares
      columns; currentCriteria still carries the snapshot so a newly named
      view keeps the layout it was saved with; applyPCriteria restores a
      view's own layout or the default; the Fields menu's "Make this the
      default layout" button is gone (All cars IS the default). Filters
      keep the Custom then name it model: a filter is what you are
      looking at, a layout is how the table is arranged. Proven live: a
      resize on All cars kept the chip on All cars and updated the
      default; on a named view it wrote into that view and survived a
      reload; All cars afterwards showed the default width.
  - BIDBRAIN INSIDE DEALER OS, THE COCKPIT REBUILT PIXEL FOR PIXEL WITH A
    TWO WAY LINK, 2026-09-02 (Steven: "i want to do number 1, come up with
    a comprehensive plan", then "build the plan"; decisions: Dealer OS
    becomes the only screen, the Mac Studio's own cockpit stays as the
    safety net until Dealer OS has proven itself over days or weeks, the
    Mac asks Dealer OS (polls, no inbound tunnel), pixel match, built here
    on branches for Mark to review). The plan lives in the session's plan
    file; phases 0, 1 and 2 are built, phases 3 to 5 (run control from
    Dealer OS, settings both ways, retiring the cockpit) are not.
    - THE PUSH WIDENED (BidBrain, `bidbrain/dealer_os_push.py`, schema
      version still 1, every field additive): `car_record` now carries
      everything the cockpit card and its record drawer draw (`key`,
      `platform_key`, `name`, `search`, `make_canon`, `model_family`, the
      engine/gearbox labels, `reserve_label`, `photo_urls`, `insights` as
      typed traffic light rows, `pricing.spread`, the headroom and Cazana
      sort keys, taste, Top picks rank, `is_new`, `seen_before`,
      `starred`, `cazana_url`, and `drawer` = `render._drawer_data`
      verbatim), computed by the SAME render.py helpers so Dealer OS never
      re-derives anything; `page_record` carries settings, notices, model
      families, filter scope, platforms, integrations and the run checks.
      `daily_run._annotate()` was pulled out of `_write_pages` so the on
      demand "Dealer OS" button sends the identical annotated, hide
      filtered document the run push does (before this it sent
      unannotated cars and cars hidden since the run). A real document
      is about 5 KB per car, 0.6 MB for a normal day; over 3.5 MB the
      photo galleries are trimmed and the push retried once.
    - THE WAY BACK (`bidbrain/dealer_os_sync.py`, new): Dealer OS is the
      source of record for anything a person does; every star, hide,
      unhide, saved view, display setting change and run request made
      there is logged, and this Mac pulls the log (`GET /api/bidbrain/
      actions?since=<seq>`) after a heartbeat (`POST /api/bidbrain/status`,
      the run light, progress and cursor; the reply says how many actions
      wait). Each action is applied through the SAME functions the
      cockpit's own buttons use (`db.set_bid` plus `serve.queue_star_
      watchlist`, `db.hide_car`, `db.save_view`, `db.set_settings`,
      `serve._start_run`), stamped with the action's own time so the echo
      never bounces; last write wins per key. `db.py` gained `updated_at`
      on bids, hidden_cars and saved_views (plus a shared `remote_id` and
      a delete tombstone on views) and `settings.updated_at` reads. The
      Mac echoes its own state back in every push under `state`. `serve.py`
      runs one daemon thread: heartbeat every 15 s (5 s busy), pull when
      something waits or every 5 min, rebuild `cockpit.html` after an
      applied action, re push the whole document at most every 30 s when
      a local change made it dirty, never during a run; a 404 from an un
      updated Dealer OS is waited out quietly (no red light), not flagged.
      `run()` pulls before every read. New `dealer_os_pull` registry
      entry and `--dealer-os-pull`. Contract in `DEALER_OS_PUSH.md`
      (mirrored into Dealer OS's `BIDBRAIN_PUSH_CONTRACT.md`).
    - DEALER OS SIDE (branch `bidbrain/cockpit` off `origin/main` v1.16.2
      in `~/Dealer_OS`, three commits, NOT pushed, for Mark's review):
      migration `20260902200000_bidbrain_sync` (BidBrainStar/Hide/View/
      Action/Status, RLS like every other dealer table), `src/lib/
      bidbrain/sync-store.ts` (state upsert plus log row in one
      transaction; the Mac's echo applied where newer, never logged),
      `machine-auth.ts` shared by the push and the two new PUBLIC_
      EXCEPTIONS, session gated routes for stars, hides, views, state,
      settings, runs; the rebuilt page behind `Dealer.settings.bidbrain.
      newCockpit` (or `?v=2`), the first cut kept as `legacy-page.tsx`;
      `cockpit.css` GENERATED from render.py by `scripts/bidbrain-
      extract-css.py` (sentinel theme values mapped onto Dealer OS's own
      `--price/--pill/--accent/--page/--sidebar`, logos onto `public/
      bidbrain/` decoded by `scripts/bidbrain-decode-logos.mjs`, all
      nested under `.bb-cockpit`), the view engine as a pure unit tested
      port (`src/lib/bidbrain/view.ts`, vitest added), one reducer, the
      card family, gauge, badges, countdown, plate, More drawer with
      twin native range sliders, views chip, top bar slot (`src/
      components/admin/topbar-slot.tsx`, the cockpit lends Dealer OS's bar
      its search, star and run button), record drawer, pop-up, gallery,
      hidden list, a 45 s state poll and sessionStorage.
    - PROVEN: 1,093 BidBrain checks pass (new: the sync's apply and last
      write wins against a temp database, the transport with a fake
      opener); Dealer OS `tsc`, `eslint` and 15 vitest checks clean; a
      LOCAL Dealer OS (Postgres 16 via Homebrew, the `dealeros_app` role so
      RLS is real, migrations applied, one dealer row) took a real
      widened push (118 cars, 595 KB, the Mac's 10 stars, 14 hides and 3
      views landing in the new tables), answered the heartbeat and served
      a test action back to the Mac's pull (into a throwaway database);
      a static render harness (`BIDBRAIN_HARNESS=<push.json> npm test`,
      served on 8772) put the real components beside the real cockpit for
      the same car and every measured property matched (font, weight,
      size, colour, radius), only the dealership theme colour and the
      live zoom script differing as expected.
    - THEN OPENED LOGGED IN, same day: the keys were on Dealer OS's own
      Settings > Team & roles > Developer access page (public by design,
      Mark's pointer), so the local Dealer OS got Steven's user row and he
      signed in himself. Three real bugs on the first load, all fixed:
      the top bar slot rendered outside the cockpit's React context (a
      runtime error, the slot now carries the context with it), it
      painted before the bar had mounted (a hydration mismatch, gated on
      mount), and `public/bidbrain/` was read by proxy.ts as a dealer
      slug so every logo 404'd (moved to `public/media/bidbrain/`, the
      path the proxy leaves alone). Then, through the real routes: star
      (badge 11), the More drawer (Any automatic: 33 cars, chip, count
      and footer agree), a view saved and deleted, a hide moving the car
      to the hidden list and an unhide bringing it back. Details opened
      the pop-up because Steven's own details_style is "popup". Branch
      pushed and the PR opened for Mark (Dealer OS v1.17.0). A test
      slip, undone: the delete test removed his real "cazana less than
      14k" view in the LOCAL database only, restored there.
    - NOT YET: phases 3 to 5 (run control from Dealer OS, settings both
      ways, retiring the cockpit) and the BidBrain release (local commits
      on main, not pushed).

