# Dealer OS and the Mac Studio: what talks to what

A short handover for a future session. The full contract (every field, every
shape) is `DEALER_OS_PUSH.md`. This file is the map: what Dealer OS can send,
what it was proven on, and what to watch out for.

Last updated 2026-09-06, BidBrain v3.14.15, Dealer OS v3.13.0.

## The shape of it, in one paragraph

The Mac Studio keeps every scraper and the whole buying brain. After each run
(and within about 30 seconds of any local change) it POSTs one JSON document to
Dealer OS with the shortlist, the purchases, the under offer and lost lists,
the page settings and the Mac's own settings. Anything a person does in Dealer
OS is written to an action log there; the Mac polls that log every 15 seconds
(5 while busy), applies each action through the very same functions its own
pages use, and pushes the result straight back. Dealer OS never re-derives a
price, a gate or a suggestion, and never writes to a platform.

## What Dealer OS sends, and where each is proven

Every action carries `by.email`, which the Mac writes as `who`. A refusal is
logged with its reason and skipped, never retried.

| Action | What it does | Proven live |
|---|---|---|
| `star.set` | shortlist a car (also queues the platform watchlist sync) | v3.11, 2026-09-02 |
| `hide.set` / `hide.clear` | hide a car for good, or bring it back | v3.11, 2026-09-02 |
| `view.save` / `view.star` / `view.delete` | the cockpit's own saved views | v3.11, 2026-09-02 |
| `run.start` / `run.stop` | ask the Mac to run something, or stop it | v3.11, 2026-09-02 |
| `settings.set {key, value}` | the two display choices (details style, price ceiling) | v3.11, 2026-09-02 |
| `purchase.set {id, field, value}` | the everyday purchase fields | v3.11.2, 2026-09-02 |
| `purchase.override {id, field, value}` | retail estimate, due in date (null clears) | v3.13.3, 2026-09-04 |
| `purchase.comment {id, text}` | a note on the car's timeline | v3.13.3, 2026-09-04 |
| `purchase.chip {id, amount, reason}` | an agreed price reduction | v3.12.1, 2026-09-03 |
| `purchase.cancel {id, reason_type, reason_text}` | the sale fell through | v3.12.1, 2026-09-03 |
| `purchase.checkin {id}` | the car arrived, becomes stock | v3.12.1, 2026-09-03 |
| `purchase.add {reg, name, price, ...}` | a car bought outside the platforms | v3.12.1, 2026-09-03 |
| `purchase.dk {id, action}` | the DealerKit buttons (price, photos, documents, retail, check; `due_in` refused since v3.14.2) | v3.13.5, 2026-09-05 (price rebuilt, see below) |
| `purchase.dk_send {id}` | the car sent to DealerKit whole, once the collection date is confirmed | v3.14, 2026-09-05, fake car by hand, four rounds |
| `purchase.dk {id, action: chip / transport / checkin / remove}` | the four follow ups on a car already on DealerKit | v3.14, 2026-09-05, all four plus every refusal |
| `purchase.checkin {id}` | now also checks the car in on DealerKit when it is on there | v3.14, 2026-09-05 |
| `purchase.delete {id}` | a hand added car leaves the list for good (timeline kept) | v3.14, 2026-09-05 |
| `views.save` / `views.delete {page, name, criteria}` | saved views on Purchases and Lost | v3.12.1, 2026-09-03 |
| `settings.set {section, value}` | dropdown answers, column layout, journey, buying rules, integrations | v3.13.1, 2026-09-04 |
| `settings.set buying_rules` with one platform | one platform's gate or switch alone | v3.13.2, 2026-09-04 |

Every action in the table has now been clicked at least once on the live
Dealer OS page and confirmed on the Mac. The first six went through on
2026-09-04 on one real car (FX68KAE, a Carwow Ford Focus): both overrides, a
comment, and all five DealerKit pushes, which uploaded 4 photos and 13
documents, corrected a retail price and wrote a due in date onto the real
DealerKit record.

**A second, fuller round on 2026-09-05, guided one step at a time by Steven
on a fake car added by hand (S17OUG), specifically to prove the handful of
things that had never genuinely been driven from a real Dealer OS click
before.** Every step passed; two real bugs were found and fixed along the
way, both below.

**A third round on 2026-09-05, the DealerKit tab redesign (v3.14),** on the
same fake car added by hand four times over: sent whole, chipped, a
transport fee sent by the AUTOMATIC run, cancelled and removed, deleted;
then sent again and checked in with DealerKit following; every refusal
sentence captured on the way. See `DEALER_OS_PUSH.md`, "DealerKit, sent
whole then kept in step". Test vehicles 320 to 325 in DealerKit were all
deleted afterwards and every trace of S17OUG removed from the Mac.

## The Mac sends back

One document per push: the shortlist and held cars, `purchases`,
`under_offer`, `lost_bids`, `purchase_events` (the timeline),
`purchase_settings`, `lost_bids_views`, `mac_settings` (the whole Settings
page including the read only fee bands, engine bans and run times), plus
`state` (the Mac's own stars, hides and views) so nothing bounces.
`schema_version` stays 1 and every change is additive.

Document scans and purchase photos live as files on the Mac, so before each
push any new one is uploaded once to Dealer OS
(`PUT /api/bidbrain/documents`) and the push carries the lasting link. 917
files went across on 2026-09-03.

## Things a future session should know

- **DealerKit's Add Vehicle needs a registration its own lookup knows
  (found live 2026-09-06 on the test car TE57DOS, a made up plate).** The
  window answered "We couldn't find a vehicle matching those details, use
  the form below to describe it" and created nothing. Until v3.14.10 that
  read as "got no confirmation, the page may have changed" and never
  reached the car's timeline, so Dealer OS's tab went back to Ready. Now
  the send waits up to 15 seconds for DealerKit's answer, looks the plate
  up before calling it a failure, carries DealerKit's own words, and every
  failed send is written to the timeline by BidBrain ("DealerKit send
  failed: ..."); Dealer OS v3.5.1 shows it on the tab with Send left on.
  A real car bought from a local trader has a real plate, so this only
  bites a made up one. Filling DealerKit's "describe it" form by hand is
  not built.
- **A cancelled hand added car can be deleted from Dealer OS since its
  v3.5.1** (the three dot menu used to vanish with the cancellation). Order
  on a car that never reached DealerKit: cancel, then Delete this car;
  Remove now is not offered because nothing went across.
- **The whole DealerKit path proven from Dealer OS on a real plate,
  LO68GXE, 2026-09-06 (a hand added car, Mac Studio car 240).** Add a car,
  due in date, three photos, Send to DealerKit (record created as vehicle
  336 with the mileage typed beside the registration, price, fee, retail
  and photos all stamped), Check now (dk_check with the vehicle number and
  photo count), Cancel sale, Remove now (DealerKit record deleted while
  Due In), Delete this car. Each step came back in the push within about a
  minute. Two things a send leaves behind that nothing from Dealer OS
  removes: the supplier contact the send creates in DealerKit when it is
  new ("Test Trader", contact 243 that day), and the photo files in Dealer
  OS's own media store.
- **Deleting a hand added car takes its own cancellation record with it
  (v3.14.11).** Cancel then delete used to leave the plate in walked_away
  for good: off every future shortlist and a grey shell row in the
  Cancelled view. To clear a plate left that way by an older version, add
  it by hand again and delete it.

- **A purchase edit made on the Mac's own page did not reach Dealer OS until
  the next daily run (found and fixed 2026-09-05).** Stars, hides and cockpit
  views always marked the push as needing to go out again; every purchases
  page write (a field edit, a chip, a cancel, a comment, an override, add or
  delete by hand, the page settings) did not. A registration correction made
  directly on the Mac's own page sat unsent for 12 minutes. Fixed once, in
  the request dispatcher (`serve.py`, `_DEALER_OS_DIRTY_ROUTES`), rather than
  in each handler, so a future route cannot forget it. v3.13.4.
- **DealerKit refuses to save a brand new car's Purchase Price unless a
  "Bought From" contact is also picked first (found live 2026-09-05, by
  Steven, testing by hand after three other real bugs in the automation were
  already fixed and the price still would not persist).** The Price button
  now selects the purchase's own known supplier (or Motorway/CarWow by
  platform) before typing the figure, the same real, live typeahead search a
  person uses. Along the way, two more real automation bugs were caught and
  fixed: the code had started typing the price into a NEW "Purchase Invoice
  Ref #" field DealerKit added since this was last built, and, once that was
  fixed, a second bug had it matching a giant, invisible ancestor of the
  whole page instead of the real, short label, because that ancestor's own
  text happened to contain the words "Purchase Price" somewhere deep inside
  it. All three are fixed together. v3.13.4, proven live on the same run
  that found them: FX68KAE's own real price was already correct before
  this, so the fresh proof is S17OUG, the fake test car this whole session
  ran on, vehicle 291 in DealerKit's own
  account, £68,000 attributed to Motorway, deleted afterward.
- **A genuinely new supplier now gets created in DealerKit automatically,
  not just matched against an existing one (v3.13.5, 2026-09-05, Steven:
  "the fix needs to be that it actually adds the supplier first ... so it
  works").** When the Price button's own search finds no matching contact,
  it creates one for real, via DealerKit's own "+ new contact" flow on the
  Bought From field (Company Name plus Contact Type "Supplier"; Referral
  Source looked optional on screen but DealerKit's own server refused the
  save without one, "Other" is used there), then carries straight on. This
  is DealerKit's own SPA at its most fragile: the "+" button shares a
  screen position with a completely unrelated "+ Add Invoice Item" button
  once the Bought From dropdown is already open, and the resulting "New
  Contact" modal renders STACKED on top of the still-open Stocklisting one
  at the same z-index, so a naive "first visible dialog" check finds the
  wrong one. Found to occasionally need a genuine second try (about 1 in 6
  real attempts), so it retries once, the same discipline this whole file
  already uses elsewhere. Proven live: 6 fresh attempts in a row, each
  with a real, never-seen-before supplier name, all created in DealerKit
  and all six prices saved correctly. 13 real test contacts (all plainly
  named "BidBrain ...") this created were removed afterward, see
  `delete_contact`, v3.13.6.
- **A DealerKit contact can now be deleted, v3.13.6** (Steven: "to delete a
  contact inside dealerway you go to contacts, click the contacts name
  then click delete"), a real, plain Delete button on the contact's own
  page (`/contacts/{id}`), the same confirm-dialog-then-verify-via-the-API
  discipline as deleting a vehicle. Used to clean up all 13 test contacts
  from the entry above; one of the 13 (id 228) genuinely no longer
  existed by the time it was tried, a real 404, most likely a duplicate
  attempt during testing that never actually landed, nothing missed.
- **Newest wins, by the action's own time.** A setting applied from Dealer OS
  is stamped with the action's `at`, never the Mac's clock. Stamping with the
  Mac's clock made two saves seconds apart skip each other (found live
  2026-09-04, fixed in v3.13.1). Stars, hides and views already did this.
- **A DealerKit push can report success without sending anything.** Three of
  the five push items can legitimately do nothing: the purchase price and the
  retail price when DealerKit already holds the same figure, and photos or
  documents when there is nothing new to upload. Until v3.13.3 all of that
  read as "Sent to DealerKit: price", and the retail price gave a reason that
  was untrue ("BidBrain had no retail estimate"). Each item now reports what
  actually happened, in the log and on the car's timeline: set, changed from
  X to Y, already right, or nothing to send.
- **The retail price rule has two halves (Steven 2026-09-04).** The all in one
  push still leaves a retail price DealerKit already holds alone, because that
  figure is the dealer's own market call. Ticking Retail price on its own is a
  deliberate instruction and DOES overwrite, so changing the figure in Dealer
  OS and sending it makes DealerKit match.
- **A due in date written to DealerKit may not be removable.** It can be
  changed to another date; clearing it has never been proven. Treat that
  button as one way.
- **A click reporting success is not proof.** Motorway and Carwow both keep
  showing the old shortlist state for a few seconds after a click, so a check
  made too soon calls a working click a failure. Both setters now wait for the
  state to change, then confirm with a reload, retrying the read (v3.13.2).
  The same discipline applies to every DealerKit write.
- **Dealer OS pages can serve a cached render.** After a push, a plain reload
  of the Settings page showed the old values; a fresh address (a query string
  on the end) showed the new ones. Check with a fresh address before believing
  a value did not come across.
- **The purchases page's own guards are the real ones.** An answer that cars
  still carry can only be retired, never removed, and Status must keep one
  finished answer. Dealer OS greys these out too, but the Mac refuses them
  regardless, with the sentences quoted in `DEALER_OS_PUSH.md`.
- **One real gap left on the Dealer OS side, found 2026-09-05.** A hand
  added car's own registration cannot be edited from Dealer OS at all, only
  from the Mac's own purchases page (no `purchase.set` field covers it
  today, `reg` is one of the row's own editable fields on the Mac). The
  other gap found that day, deleting a hand added car, is now
  `purchase.delete` (v3.14).
- **DealerKit names its expense category groups differently per account
  (v3.14).** The four purchase lines (Chassis, Buyers Premium, Delivery
  Charge, Assurance / Indemnity Fee) sit under "Stock Purchase" on Right
  Drive's account and under "Cost of Purchase" on Really Easy Car Credit's;
  the code tries both. A third dealership may need a third name added to
  `_EXPENSE_CATEGORY_GROUPS`. The contact spelling differs too ("CarWow"
  versus "Carwow"), so supplier matching is case insensitive.
- **The mileage never reached DealerKit before v3.14.** DealerKit writes a
  guessed figure onto a record created without one. The other half of the
  rule stands: a car with no mileage can never create a record.
- **What "checked in" means on DealerKit, and how it happens (v3.14,
  changed in v3.14.2).** The car moves from Due In to In Stock with its
  arrival date on the record. DealerKit does that by itself the moment the
  record's Due In / In-stock On date is today or past, and HIDES its
  CHECK-IN button once any date is set. Steven's decision on seeing this:
  the due in date is NOT sent to DealerKit any more, so the button stays
  and the check in is a person's decision, done through that button when
  BidBrain or Dealer OS checks the car in. A record still carrying a date
  is moved by writing the arrival day onto it. Proven end to end from the
  Dealer OS door on a fresh photo bearing record (S17OUG, seventh live
  round): the car moved Due In to In Stock with the arrival day and the
  mileage on the record. One thing to know about that button: DealerKit
  draws the lower half of the overview (the Due In banner that holds
  CHECK-IN, Vehicle Details, Financials) only once it scrolls into view,
  so a page nobody scrolls never shows it. A person never notices; the
  automation scrolls first. Nothing was ever wrong with any record.
- **The mileage is typed beside the registration at creation (v3.14.2).**
  The mileage log entry is only the fallback for a record DealerKit already
  had with a different figure.
- **A removal is keyed by registration.** The Ford Puma (ND20NVZ) was
  bought on both platforms the same day, one sale cancelled and one live;
  removing the cancelled sale's "DealerKit record" would have removed the
  live one's. Refused with a sentence, in the door and in the automatic
  run. The automatic run also treats a follow up that finds no record at
  all as "not on DealerKit" and stamps `dk_removed_at`, so it does not ask
  again every day.
- **The automatic DealerKit run fires at 16:15 and never sends a car
  whole (v3.14.3).** A car goes to DealerKit only when a person presses
  Send in Dealer OS; the run picks up chips, transport fees, check ins,
  removals and (v3.14.4) retail prices that differ from the last DealerKit
  check, on cars DealerKit already has. An explicit retail job from Dealer
  OS always overwrites DealerKit's figure; only a send whole leaves it. The Send preflight's "no
  collection date yet" reads `collection_date` (the due in date) only,
  never `collection_arranged`.
- **The Mac Studio's own screens stay live.** Nothing has been retired, and
  nothing should be until Steven has said so twice in his own words. All six
  pages (cockpit, purchases, lost bids, under offer, settings, hidden) were
  confirmed working on 2026-09-04.
- **Two dealerships share this repo.** Right Drive (Mark) runs the same code
  with its own `dealer_config.py` and its own Dealer OS token. Anything added
  here has to work for a dealership that has not configured it.
- **Where things live.** The way back is `bidbrain/dealer_os_sync.py`; the
  doors it calls are in `serve.py` (`apply_purchase_field`,
  `apply_purchase_action`, `apply_settings_section`, `_Doors()`); the push is
  `bidbrain/dealer_os_push.py`; the file uploads are
  `bidbrain/dealer_os_files.py`.
- **A test that takes the default database path writes into the live
  database.** Found 2026-09-05: three settings sync checks had been reading
  the Mac's real stamps and one wrote `details_style` with a year 2099
  stamp, which would have blocked every later Dealer OS change to it. Every
  check that touches settings, bids, hides or views must pass its own
  temporary path, and the suite is hashed against `data/bidbrain.db` before
  and after a run.
- **Test the suite's own end.** `test_pricing.py` prints its summary and
  exits at the very bottom on purpose. Anything appended after that never
  runs (it happened once, and a block of checks sat unrun for a day).
