# Changelog

BidBrain doesn't follow semantic versioning against a public API, this is a
private buying tool, not a library. These are just the project's own build
stages, each covering everything shipped up to that point. For the full
decision log behind any entry here, including exact live test numbers and
the reasoning behind edge cases, see `CLAUDE.md`.

Bug fixes are called out under **Fixed** in each version; everything else is
a new capability (**Added**) or a change in existing behaviour (**Changed**).

## v3.14.154, 2026-10-05

### Fixed
- A history check on the wrong car no longer goes to DealerKit. Steven,
  on S77RUU: the check ran on the seller's private plate after it had
  moved to a 2025 Mercedes CLA, and the report was lined up as the
  history of the 2017 Mercedes A we bought. Dealer OS now checks the
  plate that stays with the car and compares every report with the car;
  when it takes a report back, BidBrain drops the file from the car so it
  is never sent, and says so on the car's timeline. Same for both
  dealerships.

## v3.14.153, 2026-10-05

### Fixed
- PL66ZZO now shows its £200 chip. It was won on Motorway at £6,456 and
  chipped £200 through Motorway before v3.14.152, so BidBrain had £6,256
  as the winning bid and no chip. On the next start the winning bid goes
  back to £6,456 and the chip is recorded as "Price amended on Motorway",
  from Motorway's own payment email. The price paid stays £6,256, £6,650.80
  all in. Steven chose this car only; other older Motorway cars stay as
  they are.

## v3.14.152, 2026-10-05

### Fixed
- A chip put through Motorway was lost from the purchase price. Steven, on
  PL66ZZO: won at £6,456, chipped £200 through Motorway at collection, and
  Purchases already showed £6,256 as the winning bid with no chip, before
  Donna could record it. While Motorway's breakdown is still estimated it
  is re read, and its "Vehicle price" line simply drops to the amended
  figure. A real breakdown's bid that goes down now keeps the bid as won
  and records the drop as a chip, "Price amended on Motorway", so Dealer
  OS's Collected step shows the chip already there. A price that goes up
  is still taken as read.

## v3.14.151, 2026-10-04

### Fixed
- Carwow cars in the live auctions showed as seen before, twice today.
  Steven, on LC66AZW: "there seems to be alot of cars in the live auctions
  that are showing as seen before but its shown twice today?". Carwow holds
  no sale on a Sunday, so Saturday evening's run and Sunday's run both read
  Monday's sale, but Saturday's was filed under Sunday. Every Carwow car
  still on sale on Sunday evening then looked like it had already been
  through a Sunday sale: 54 of the 80 Carwow cars on Monday 5 October's
  list. A Carwow read stamped Sunday is now filed under Monday, and the
  sightings already logged under a Sunday move to Monday on the next start.
  Motorway does sell on a Sunday and is unchanged.

## v3.14.150, 2026-09-29

### Fixed
- Seller notes on Carwow and Motorway never reached the car page. Steven,
  on FY16BHD: "this passat has seller notes on carwow that put me off
  bidding for it but yet bidbrain doesnt tell me". Carwow shows them under
  the car's features ("Seller notes: Clutch pedal squeaks, rattle coming
  from the airbag area"), cut short behind View more when long. Motorway
  keeps them in Additional details: the extras the seller ticked, anything
  written after them, and why a car was relisted. BidBrain now reads all of
  them on the page visit it already makes, and they show as Seller notes on
  the cockpit card and the Dealer OS car page.

## v3.14.149, 2026-09-29

### Fixed
- Carwow cars showed only a distance on the car page, never the town.
  Steven, on SA18AJX: "why are some cars only showing the distance as the
  location and not showing the city or the actual place as well?". Carwow's
  list card only says how many miles away the car is. The town is on the
  car's own page, in its Location section ("Elton, Bury (134 m)"), and
  BidBrain never read it. It now reads it on the page visit it already
  makes, so Carwow cars carry their town into the cockpit and Dealer OS.
  A car whose town is now known also gets the ferry crossing check.
- A Carwow car's page listed only its current owner, however many it had.
  Steven, on the same car: "it only shows the current owners time of
  ownership, it should show all owners". Carwow's car page has an
  Ownership history section with every owner's dates, and the deep read
  only took the current keeper's start. It now reads the whole section,
  oldest first. Motorway cars already read every owner (checked live).

## v3.14.148, 2026-09-28

### Fixed
- A car whose listing plate could not be read was called new every day it
  came round. Steven, on S30NNT in the Carwow sale: "It says its not been
  seen before but i could swear i have seen it before". Carwow's card did
  not show a plate BidBrain could read, only the car's own page gave the
  original plate underneath, and the repeat tracker skipped any car with no
  listing plate. So it was never logged on any day it appeared, and the
  card said First time. Six Carwow cars on the 28 September list had no
  listing plate, five of them with an original plate on their page.
- Sightings are now logged under the original plate when the listing plate
  is missing, once the car's page has been read. Seen before, the reserve
  drop and Dealer OS's Seen list all look a car up by both of its plates,
  and by the other half of any private plate pair on file, so a car listed
  on its private plate one day and its original plate another still meets
  itself.
- A car with no plate at all is no longer called new. There is nothing to
  look it up by, which is not the same as seeing it for the first time, so
  it no longer gets the New today badge or the new car boost in Top picks.

## v3.14.147, 2026-09-22

### Changed
- The Dealer OS push now goes compressed. Plain JSON squashes about ten
  to one, so the biggest real day seen so far, 2,078 held back cars and
  5.17 MB of list, leaves the Mac as roughly 555 KB. This was tried and
  reverted on 21 September because Dealer OS read the bytes exactly as they
  arrived and answered "Body is not valid JSON"; Dealer OS has since
  fixed its side, so all four of its machine doors decompress the body
  and measure the size after decoding. Checked against Dealer OS's own
  reader with the real bytes this sends, not a stand in.
- Because of that, the 200 row caps on held back cars and lost bids put
  in on 21 September no longer bite. The whole held back list goes across
  again. The caps are still in the code as a genuine last resort, behind
  two ceilings that are now both checked before a push leaves: 19 MB for
  the document itself, a margin under the 20 MB Dealer OS stops
  decompressing at, and 4 MB for what actually travels, a margin under
  Vercel's own request cap. A real day is nowhere near either.
- The run summary line now says both sizes, what was sent and what it
  was before compressing, so anyone can see at a glance if the
  compression ever stops happening.

### Fixed
- A Dealer OS too old to read a compressed push used to come back as a
  bare "answered 400". It now says in words that the Dealer OS side
  needs updating first.
- A deep read of a Carwow car never read its MOT. The reader looked for
  labels Carwow does not use, so MOT history said "Not read yet" on every
  Carwow car on the shortlist. It now reads Carwow's own MOT panel: the
  date of the last test, pass or fail, the expiry and that test's
  advisories. Checked live on PH58EEE and BL18YCN.
- Wheels and tyres said "Not read yet" on every car after a deep read.
  Carwow's Wheels and Extras panel is now read (tyre damage, alloy damage,
  a missing locking wheel nut or toolkit), and a Motorway or Carwow car
  whose wheels are all clear now says "No problems declared" instead of
  looking unread.

## v3.14.146, 2026-09-22

### Fixed
- **A freshly won Motorway car came in about £400 too cheap.** Steven:
  "the purchase price of BC17OXN has came over into DOS totally
  incorrect". BidBrain read the Motorway purchases list figure as the all
  in total every time, and worked the car's own price back out of it by
  taking off the banded buyer's fee and its VAT. That is only true once
  Motorway has worked the fee out. While a car is still going through,
  Awaiting confirmation or Reviewing documents, there is no payment
  breakdown yet and the list figure already is the bid, so about £400 was
  coming off a number that never had it on. BC17OXN read £7,636 instead of
  the £8,066 Steven bid, and its all in read £8,066.80 instead of
  £8,496.80. Checked against Motorway's own sale recap emails for every
  car bought between 10 and 21 September: every car still going through
  was short by its own fee and VAT, and every completed car was right, so
  the fault only ever showed on the newest buys and cleared itself once
  Motorway published the breakdown. `db.provisional_motorway_bid` now
  takes the row's status and only takes the fee off once the fee is in the
  figure (`db.motorway_fee_in_list_price`), `purchases_run.estimated_fee`
  passes it through, and `list_purchases` builds the all in up from the
  car price on a car still going through rather than using the list figure
  as the total. The seven live cars still on a pending status correct
  themselves on the next purchases run. 10 new checks in test_pricing.py
  (1,797 passing). Touches Mark: yes, it is Motorway's own behaviour, so
  his copy reads the same cars the same way; no setting, no default, no
  scheduled job, and nothing here acts on a platform.

## v3.14.145, 2026-09-22

### Added
- **The seller's modifications answer now travels with a starred car's
  deep read.** Steven asked why Modifications was missing from the new
  shortlist car card in Dealer OS (2026-09-22): the Mac never read it, so
  there was nothing to show. When the Mac reads a starred car's listing
  in full it now reads whether the seller declared any modifications,
  on Motorway from the car's own page data and on Carwow from the
  Modifications line, and sends it up as `deep.modifications`: the
  seller's own words when there are any, "None declared" on a No, and
  nothing at all when the page did not say, never a guess. Dealer OS
  v4.5.26 shows it as the tenth fact on the car card. Motorway's own
  name for the answer has not been seen live yet, so the read tries the
  names it would plausibly use and the first live read writes every
  path with "modif" in it to data/logs/deep_read.log, so the next fix,
  if one is needed, needs no guessing. Touches Mark: his Mac reads the
  same extra line on the cars he stars. No new setting, no new default,
  no new scheduled job, and nothing here acts on a platform.

## v3.14.144, 2026-09-21

### Fixed
- The run bar used to vanish the instant a run finished, no trace of the
  real result anywhere except a toast gone after 5 seconds. Reproduced
  live on Didn't win full history, the bar hid the exact same tick the
  job finished. A finished run's own real result now stays in the bar
  for 8 seconds, the same brief treatment an error already got. A second
  gap found proving this live: nothing polled a finished run's own watch
  again once it stopped running, so the bar could get stuck showing that
  same old result forever, confirmed still there over 30 seconds later.
  One more poll after the 8 second window now retires it, or hands off
  cleanly to a genuinely new background push that has since taken the
  bar over.

## v3.14.143, 2026-09-21

### Fixed
- The Dealer OS push was down since this morning, the list too big to
  send. A genuinely large day, 2,078 held back cars plus a normal day's
  purchases and lost bids, came to 5.17 MB, and the old photo trim only
  ever saved 104 KB, nowhere near enough. Even with held back and lost
  bids dropped to nothing at all, the live auction list and purchases
  alone already came to 3.54 MB, past the old 3.5 MB cap before any
  trimming happened at all, since that cap was set when a normal day was
  well under 2 MB and never revisited as the push grew. Raised to 3.85
  MB, a real margin under Dealer OS's own stated limit of about 4 MB. A
  genuinely large day now also caps the held back and lost bids lists to
  200 rows each as a last resort, held back already sorted cheapest
  first so the cars nearest the buying threshold survive, lost bids
  keeping the most recent. Purchases and every settings block are never
  capped, Dealer OS draws its own pages from them whole.

## v3.14.142, 2026-09-20

### Added
- **The same car won on two platforms is now caught, named and held.**
  Every platform keeps its own purchase row, so a car won on both
  Motorway and Carwow was two live records and nothing joined them up.
  It has happened for real twice (ND20NVZ and the Ford Puma) and the
  only reason it did not cost money is that somebody noticed. Now the
  purchases page carries a red warning at the top the moment a second
  live win lands on a plate that already has one, and both rows carry a
  tag on the registration: green Keep on the cheaper sale all in, red
  Cancel on the dearer one. The warning says in one sentence which to
  keep, which to cancel and what cancelling saves. Both timelines get a
  line saying it happened, the minute it happened.
- **Nothing moves on either car until one sale is cancelled.** Neither
  goes into the accounts email, so accounts are never told to pay twice
  for one car, and DealerKit refuses to take either one whole, so it
  never ends up with two records for one car. Cancel the losing sale on
  its platform, cancel it here, and the warning and both holds clear
  themselves. There is nothing to remember and nothing to undo.
- The warning shows in Dealer OS as well as on the Mac's own page, with
  the same sentence in both places, because the sentence is written once
  on the Mac and sent as it is.
- BidBrain still never touches a platform. It says which sale to keep
  and why; cancelling on Motorway or Carwow is a person's job, exactly
  as before.
- Nothing is recommended when the app cannot honestly choose: a sale
  with no price on file yet, or two sales costing exactly the same, get
  the warning and both holds but no Keep or Cancel tag.
- The same plate won twice on ONE platform counts too, and so does a
  second win days later, not only two on the same day.

## v3.14.141, 2026-09-20

The Carwow shortlist sync now asks Carwow's own shortlisted vehicles
page whether a car is starred, both before it clicks and to confirm the
click. That is the very page Steven looks at, so BidBrain and Steven can
no longer disagree about whether a car is on the shortlist.

Why: v3.14.137 had the Mac read two cars' listing pages without touching
either, FL16MXJ, which Steven confirmed was on his shortlist, and
ND19HZG, which was not. The two came back identical to the character.
The car's own page on Carwow does not say whether the car is
shortlisted, so the heart on it could never answer the question, and the
sync has been calling every successful star a failure since the day it
was built.

What changes: a car already in the state you want is now never clicked
at all, which matters because the heart is a single switch and a
needless click turns a correctly starred car off. A car that needs
changing is clicked once and then confirmed against the shortlist page.
An unreadable shortlist page, a login screen, a redirect somewhere else
or a page carrying no listing cards at all each stop the car with a
clear reason rather than reading as an empty shortlist, which would have
quietly turned every starred car off. A shortlist that is genuinely
empty still reads as empty, which is a real answer.

Touches Mark: yes, his Carwow stars have never arrived either, and this
is the release that gets them there. Nothing else changes for him: no
new setting, no new default, no scheduled job, and the shortlist toggle
is still the only platform action BidBrain ever takes.

## v3.14.140, 2026-09-20

### Fixed
- A car whose seller kept their private plate stops reading as missing on
  DealerKit. The platform lists such a car under the private plate, but
  the car sells, and so reaches DealerKit, under the plate underneath
  (T18TBA selling as YL66HWE is the worked example in CLAUDE.md). Every
  DealerKit lookup matched one plate exactly, so those cars read as
  having no DealerKit record at all: no check, no chip, no transport
  invoice, nothing, for good. Every lookup now tries the plate the
  purchase is recorded on first and then the plate underneath, and the
  whole stocklist is still read only once. The DealerKit panel on a
  purchase says which plate the car was found under when it was not the
  one at the top of the card, and says both plates were tried when it
  found nothing.

### Added
- BidBrain now keeps the two plates a car is known by. It has read the
  plate underneath since June, for the valuations, and thrown it away
  every time, so nothing that looked a car up by plate afterwards could
  ever try it. Each run records the pair as it reads it, and keeps it for
  good, because a car bought today can reach DealerKit weeks later. A
  purchase picks its own second plate up from that, whichever of the two
  plates the purchase itself was recorded on. A car whose listing never
  stated one can have it typed in on the card, under Sells on plate, and
  a value that is not plate shaped is refused rather than quietly
  widening a search.

## v3.14.139, 2026-09-20

### Fixed
- The Mac reads its own version correctly when a commit carries two tags
  for the same release. v3.14.138 was also cut, by mistake, as a bare
  "3.14.138", so two tags sat on the same commit. git lists them in name
  order, the bare one came first, and the Mac would have read itself as
  "3.14.138" while the newest release said "v3.14.138". Those never
  match, so the Software update card would have offered an update that
  was already installed, again and again, and a Mac with auto update
  switched on would have restarted itself every couple of minutes. The
  real release tag, the one with the v, now wins.

## v3.14.138, 2026-09-20

### Fixed
- The stock gap badge and the top picks count one real model as one
  model. They grouped a car by the first word of the make plus the first
  word of the model on its own, so wherever a platform prints the engine
  badge where the model should be, one model was counted as two: a BMW
  118D never met the BMW 1 Series already in stock and read as a gap, a
  Mercedes A 180 never met an A Class, an MG Motor UK ZS keyed on "motor",
  and Motorway's truncated "Insig" never met an Insignia. The top picks
  score partly on that gap and on the model's own sold record, so they
  inherited it too. All of them now group through CompMatch's own
  canonical key, which already solved exactly this, so the badge, the sold
  history note, the taste match and the picks all land on the same group
  as the suggested bid. Annotation only, nothing about pricing, the hard
  gate or any buying rule changes. The learned model figures rebuild
  themselves once from the sales history already on file, so a model
  really sold before stops reading as never sold.

## v3.14.137, 2026-09-20

Carwow's shortlist panel, as Carwow serves it, now goes into
data/logs/deep_read.log every time the shortlist deep read opens a
Carwow car. The deep read only ever reads a page, it never clicks
anything, so a car that is on the shortlist can be put beside one that
is not without touching either.

Why: v3.14.135 got the click through to Carwow, and Steven's own
shortlist screen shows the cars going on, yet the page read straight
after still says the car is not shortlisted. So the click is right and
the reading of the state is wrong. No session away from the Mac can open
a real Carwow listing, so the only honest way to fix the reading is to
have the Mac report a starred car's panel and an unstarred one's and
build the check on the difference, never on a guess (golden rule 4).

What is reported, each on its own short line because a log line is cut
at 400 characters on its way to Dealer OS: the classes on the frame, on
the panel and on each marked icon, the form's action, method and fields,
any label or wording, and the words the panel shows. The forgery token
is left out, it changes on every read and says nothing about the state.

Nothing about the sync itself changes in this release, and nothing is
clicked, so Mark's copy behaves exactly as it did.

## v3.14.137, 2026-09-20

Carwow's own shortlist panel now goes into data/logs/deep_read.log every
time the shortlist deep read opens a Carwow car. That read only ever
reads a page, it never clicks anything, so a car that is on the
shortlist can be put beside one that is not without touching either.

Why: v3.14.135 got the click through to Carwow, and Steven's own
shortlist screen shows the cars going on it, yet the page read straight
after still says the car is not shortlisted. So the click is right and
the reading of the state is wrong. No session away from the Mac can open
a real Carwow listing, so the only honest way to fix that reading is to
have the Mac report a starred car's panel and an unstarred one's, and
build the check on the difference rather than on a guess (golden rule 4).

What is reported, each on its own short line because a log line is cut
at 400 characters on its way to Dealer OS: the classes on the frame, on
the panel and on each marked icon, the form's action, method and fields,
any label or wording, and the words the panel shows. The forgery token
is left out, it changes on every read and says nothing about the state.

Nothing about the sync itself changes, and nothing is clicked, so Mark's
copy behaves exactly as it did.

## v3.14.136, 2026-09-20

### Fixed
- The Carwow shortlist sync stops clicking the same car over and over.
  v3.14.135 got the click through to Carwow, and Carwow accepts it and
  answers its usual success, but the car still reads as not shortlisted
  straight afterwards. Steven then checked Carwow by eye and several of
  the cars reported as failures WERE on his Carwow shortlist. So the
  click lands and it is the reading of the result that cannot be
  trusted, and that is worse than it sounds: the shortlist button is one
  switch, so a car read as not starred when it really is starred gets
  turned off by the next attempt. From now on, when the click genuinely
  reached Carwow, that car is not tried again. It stays on file with its
  real reason, and starring it again in the app starts it over.

### Added
- A Carwow shortlist failure now carries Carwow's own panel for that
  car, the markup and all, into the run log and the error. No session
  can open Carwow to look at a real listing, so without this there is
  nothing to work from but guesses, and guessing is not allowed
  (golden rules 4 and 5). This is what the next reading of the state
  will be built on. The panel is reported as it read before the click
  as well as after it, since a car that is already on the shortlist
  only shows how Carwow marks a starred car until something changes it.

## v3.14.135, 2026-09-19

### Fixed
- Starring a Carwow car now genuinely reaches Carwow's own shortlist.
  Every Carwow star and unstar had been failing, in both directions,
  for as long as the sync has existed, while Motorway's syncs fine in
  the same pass. Carwow loads the code behind its own star button
  lazily, only once the button appears on the page, and that finishes
  after the page itself has loaded. BidBrain waited for the button's
  markup, which Carwow sends straight away, then clicked it. A click
  that lands before the button is alive does nothing at all, so
  nothing was ever sent to Carwow, and the car stayed exactly as it
  was. Read from Carwow's own published code, not guessed.
  Three changes: BidBrain now waits for Carwow itself to report the
  button live before clicking; it reads and clicks only inside the
  panel belonging to that one car, so another car's heart on the same
  page can never be read or clicked by mistake; and it confirms the
  result against a page Carwow serves fresh, rather than a reload the
  browser may answer out of its own cache. When it still cannot set
  a car, the error now says which of those three it was, instead of
  only that the car did not change.
  This fixes Mark's copy in exactly the same way, his Carwow stars
  were never reaching Carwow either.

## v3.14.134, 2026-09-19

### Fixed
- Carwow cars are back on the list. Carwow put a "view details" link
  beside the Former keepers count on the car's own page, so the line
  reads "2 view details" instead of "2". BidBrain was reading that
  line as a plain number, got nothing, and a car with no previous
  owners count fails the hard gate, so every single Carwow car was
  rejected. Four days with no Carwow cars at all, on Motorway cars
  only. The count is now read from the front of the line, and a line
  that does not start with a number still leaves the count unset, so
  a page that changes again holds the car back rather than guessing.

### Changed
- A platform whose stock reads fine but whose every car then fails
  the gate now shows as a failed read in the Run modal, with the
  commonest reason on it, instead of a healthy looking "0 cars". The
  reasons were already printed in the run log, where nobody reads
  them, which is how the Carwow break above stayed invisible for four
  days. A genuinely quiet day reads the same way, which is the point:
  it is worth a look either way.

## v3.14.133, 2026-09-19

### Fixed
- Damage on a starred car now carries the photo of that damage, on
  both platforms. The two entries below shipped together in one
  merge, so they are one release rather than two: a release only
  carries its own section into the Software update card, and split
  across two versions half the work would never have been read.

- A Carwow car's declared damage now carries the photo of that damage,
  the way Motorway's already did. Carwow does say which photo belongs to
  which fault, in its condition report markup rather than in the page's
  words, so `deep_read.from_carwow` had been listing the damage with
  `photo_url` left null and dumping every photo into one undifferentiated
  gallery. `carwow_damage` reads each damage block instead, giving where
  it is, what it is and its own photo: "Rear driver alloy: Scuffed
  alloy". Read live off listing 14611801 and proven against six live
  listings, where the count matched each page's own "N areas of damage"
  line exactly and every damage had a distinct photo. The page also
  carries a damage slideshow, deliberately not used: it repeats its
  slides to loop, so going by position would pair the wrong photo with
  the wrong fault. A page with no condition report still falls back to
  the page's words exactly as before, so nothing that read today stops
  reading. Nothing here is dealership specific, so this reaches Mark's
  Mac and Steven's the same way, with no new setting and no new run.

- A Motorway car's damage was being listed twice over and padded with
  wheels. Motorway splits a damage in two, the words in
  `conditionAndDamage` ("Dents", "1 small (0-5cm)") and the picture in
  the gallery (`kind: damage_dents`), and the read handed both halves to
  the screen as separate entries: the words with no photo, then a bare
  label with the photo. On top of that any gallery photo flagged
  `isDamaged` counted as bodywork damage, so wheels and tyres came back
  in through the gallery after v3.14.131 had deliberately moved them to
  their own line. Read live on 2026-09-19: a car whose only fault was
  three scuffed alloys and two tyres showed five "areas of damage", and
  an Audi A5 with four real faults showed twelve entries.
  `motorway_damage` now joins the two halves on what the photo is of, so
  each damage appears once carrying both the seller's words and its own
  photo, and wheels and tyres stay out of bodywork (they are already on
  the `tyre_notes` line, and their photos are still in the gallery). The
  three live cars went from 5, 10 and 12 entries to 0, 3 and 4, every one
  with its photo. Nothing is dropped: a photo of something never declared
  keeps its own name, and a fault declared with no photo still reads.
  Nothing here is dealership specific, so this reaches Mark's Mac and
  Steven's the same way, with no new setting and no new run.

## v3.14.131, 2026-09-18

### Fixed
- Wheels and tyres, and a warning light on the dashboard, now show as
  their own lines (`tyre_notes`, `warning_lights`) instead of being
  folded into the bodywork `damage` list, where Steven found them
  buried under one "Damage" bullet and read them as missing. No
  platform gives a plain "mechanical fault" flag; what shows up about
  the mechanicals comes through in the MOT's own advisories, which
  already have their own line.

## v3.14.130, 2026-09-17

### Fixed
- The shortlist deep read's last unmapped Motorway names (fourth live
  round, 2026-09-17): the MOT block's status, last test date and
  advisories (Motorway never gives an expiry date, so none is worked out;
  the test date goes as the new `mot_tested`), the colour and interior
  trim off the specification block (`interior` is new too), and the
  wheels and tyres block's tyre problems, scuffed alloys, missing locking
  wheel nut or tools as condition lines. Motorway shows no tread depths,
  so tyres stay "not read" honestly.

## v3.14.129, 2026-09-17

### Fixed
- The shortlist deep read takes its photos from Motorway's gallery block
  (seen live 2026-09-17): every photo of the car once, large, the
  seller's documents left out, and the damage photos named and carried
  with the damage list so the screen can mark them. A declared damage
  line no longer carries a line break inside it. The run log's key notes
  show one image's worth of the gallery, not eighty lines of it, so the
  names still to map (MOT date, colour, tyres) can be seen.

## v3.14.128, 2026-09-17

### Fixed
- The shortlist deep read now reads Motorway's declared condition and
  history check by the names Motorway really uses (seen in the run log
  after v3.14.126's first live round): dents, scratches, paint, missing
  parts, windscreen, warning lights and smoked in, each a Yes with its
  description, become the damage list; hasBeenStolen, hasBeenImported,
  the two written off flags and salvaged feed the history check; the MOT
  date is taken off the HPI block when it carries one; every keeper's
  start date becomes the keepers list. Before this a car with declared
  scratches read as "None declared" and stolen or imported never showed.
  The run log now prints up to 80 names, the still unmapped ones (MOT,
  tyres, colour, gallery) first.

## v3.14.127, 2026-09-17

### Added
- The Mac updates itself (Steven 2026-09-17, "make the Mac update
  itself"): a new Connections switch, "Update itself when a new release
  is published". With it on, every two minutes while nothing is running
  the Mac looks for a newer BidBrain release on GitHub and installs it
  the same way the Update now button does, restarting BidBrain for about
  a minute. Off by default, so Mark's Mac keeps the button as it is; it
  is turned on from the Mac's Settings > Connections or Dealer OS's
  Automation switches card. A run in flight or a dirty tree is left
  alone and tried again next time.

## v3.14.126, 2026-09-17

### Fixed
- The shortlist deep read's first live round (ten Motorway cars, 2026-09-17):
  six came back blank with no error. Their page carried no car block at
  all (the sale had closed by the time they were read), and the read
  called that "read". A page with no car block is now a failed read that
  says so: "Motorway's page for this car carried no car data. The sale
  may have closed or the listing been withdrawn." The read now works
  inside Motorway's real car block (props.pageProps.vehicle) and uses the
  names Motorway really uses for the service book (serviceRecord, the
  official and independent stamp counts, the records' service centre) and
  the history check (hpiHistoryCheck's finance agreements count and
  clocked mileage flag, details.onFinance). The names it saw for the MOT,
  keys, colour, damage, tyres and photos are printed into the run log on
  every read, so the diagnostics document shows them for the next round.

## v3.14.125, 2026-09-17

### Added
- The shortlist deep read, the Mac's half of Dealer OS v4.1.0's shortlist
  screen (one starred car at a time, everything on one page). "Read this
  car" or "Read them all now" there logs a shortlist.read action naming
  only the starred cars; the sync queues them and starts a quiet run that
  opens each car's own page in the saved login, reads the MOT and its
  advisories, the service book, the keepers, colour, keys, tyres, declared
  damage, the platform's own history check, every photo at full size and
  the reserve and bid as the page shows them now, and the next push
  carries it under each car's deep block. Motorway (its page data, then
  its words), Carwow, DealerWay and Auction4Cars; Dealer Auction has no
  car page read yet and says so. A dead login or a page that will not load
  is answered in plain words, never left blank, and a field the page did
  not show is null, never a guess. Never a bid and never any action on
  the page. Asked for while the Mac is busy, it runs on the next quiet
  moment. Both Macs get it, nothing about buying, collection or DealerKit
  changes and no default moves. Motorway's real field names are written
  to data/logs/deep_read.log on every read, so the first live read
  shows which names still need mapping.

## v3.14.124, 2026-09-16

### Fixed
- Proven live: only the last of the survey's real pictures made it
  through diagnostics' own 300 line / 256KB tail window, the earlier ones
  pushed out. The tail is now 500 lines / 512KB, and the survey keeps
  pictures at its two most useful screens only (the item window, the
  Edit Expense window on an existing supplier expense), so both survive
  every time.

## v3.14.123, 2026-09-16

### Fixed
- The costs screen survey wrote every picture the walk had appended, then
  wiped them out with a final overwrite of the same file (found live: the
  first real survey after v3.14.122 shipped no pictures at all). The
  survey now appends its own text lines the same way, so nothing after it
  ever overwrites what the walk already wrote.

## v3.14.122, 2026-09-16

### Added
- A real picture, not just words, when a DealerKit step fails or the
  costs screen survey runs (Steven, 2026-09-16: "is there any way at all
  that you will be able to make it so you can sign in to dealerkit from
  here?"). No password or browser session moves off the Mac: instead the
  Mac clips a small, low quality JPEG of the window it was looking at,
  writes it as plain text (base64) into the same log files diagnostics
  already ships, and a small script on the Dealer OS side turns it back
  into a real picture. Every genuine push_followup / push_purchase
  failure carries one now; the survey carries one at each of its four key
  screens.

## v3.14.121, 2026-09-16

### Fixed
- A line left alone on the purchase expense reads "already right on
  DealerKit", not "changed from £289 to £289".
- A chip sent to DealerKit stamps the price and the fee as sent too,
  since the chip puts both there.
- A line's own menu on the Edit Expense window is looked for in that
  window only (the survey: each line is a table row with its own menu).

## v3.14.120, 2026-09-16

### Fixed
- Correcting an existing purchase expense only deletes and re adds a line
  that is there and differs; a line already at the figure is left alone
  and a missing one is just added (ND66XYJ, sixth attempt: the fee
  already matched, the delete of its line failed, and the missing price
  never got added). The line menu, Delete and Save & Approve failures now
  carry the window's words.
- The costs screen survey also writes down the Edit Expense screen of an
  existing supplier expense, with each line's own buttons.

## v3.14.119, 2026-09-16

### Fixed
- Each expense line now proves it landed before the next starts (ND66XYJ,
  fifth attempt: the Carwow expense was created with the £289 fee only,
  the £4,110 chassis line typed first was lost when its Add did not take
  and the fee line wrote over it). The Unit Net box is filled through
  Playwright's own fill and blurred, then read back; the item window's
  Add is pressed again after a blur if the window stays open; the form
  must list the line before the next one is added; every failure carries
  the window's words.

## v3.14.118, 2026-09-16

### Fixed
- Adding a line to a DealerKit expense, written from the real screens of
  Steven's account (the costs screen survey): the form's own ADD opens a
  separate Add Expense Item window with the Item Category already set
  ("Cost of Purchase > Chassis" on a fresh form), Item Description, Qty,
  Unit Net £'s, VAT Tariff preset to "Z - Zero", and its own Add. The
  category is opened only when it does not already read the line wanted;
  the Expense Categories window then takes the group and the line, scoped
  to that window so the same words behind it are never clicked; the VAT
  tariff is set outright every time (No VAT on the chassis, Standard on a
  fee); the Unit Net box and the Add button are found in the topmost
  window. Mark's older in row picker ("Pick a category") still works.

## v3.14.117, 2026-09-16

### Added
- A DealerKit costs screen survey (purchases_run.py --survey-dealerkit, the
  dealerkit_survey run): opens one car's Edit Vehicle > Expenses, presses
  + EXPENSE, picks the team and the supplier in the unsaved form, presses
  the item ADD and opens the category box, writing every screen down
  (text, buttons, fields, options) to data/logs/dealerkit_survey.log, then
  leaves without saving. So the add an expense steps can be written from
  the real screens of this account instead of one press at a time.

## v3.14.116, 2026-09-16

### Fixed
- The category box on a new expense item line is found by meaning (any
  wording with "category" in the open window), a searchable picker is
  typed into when no group list appears, and a missing Unit Net box says
  so with the window's words (third attempt on ND66XYJ: the ADD button was
  right at last, the category box was not worded "Pick a category").
- A failure's saved screen now holds the whole topmost window: its
  buttons, its field labels and placeholders, and all of its text, not
  just a warning line.

## v3.14.115, 2026-09-16

### Fixed
- The Add Expense form's item ADD button, seen for real on Steven's
  account (the window's words from v3.14.114): the form offers
  "+ EXPENSE", "+ CREDIT", an icon only "+" beside the supplier picker and
  the item row's own "ADD" at once. A button that starts another expense,
  a credit or a contact is never pressed for an item; the one reading
  "ADD" is.
- A failure's "The window says" quotes the topmost open window, not the
  Edit Vehicle menu underneath it.

## v3.14.114, 2026-09-16

### Fixed
- The new purchase expense on a car with none (ND66XYJ, first real use of
  v3.14.113's route): the item ADD button is chosen by its words, never the
  icon only "add" beside a contact picker, and the supplier is picked from
  the picker's own options, never the vehicle list behind the modal.
- When a DealerKit step cannot find what it needs, the failure now carries
  the window's own words, and the full text goes to
  data/logs/dealerkit_step.log for the next diagnostics pull.

## v3.14.113, 2026-09-16

### Fixed
- The purchase price, buyer's fee and transport fee now always reach
  DealerKit on a send (found live on ND66XYJ and FJ67WFR: the price box at
  creation saved nothing, the send then said "nothing to send, BidBrain had
  no confident figure" although it had one, the fee was refused with "send
  the purchase price first", and DealerKit held no purchase figures at
  all). One route now: a price saved at creation counts; otherwise the
  three figures go on the supplier's purchase expense together, created
  when the car has none, corrected when a line differs, left when it
  matches. A figure that cannot be written is said per line, in words,
  never silently.
- Why a price was not saved at creation is now in the log and on the
  timeline (the Bought From contact, the price box, or DealerKit not
  saving), instead of being swallowed.
- The Bought From contact is matched without regard to case or spacing
  ("Carwow" on DealerKit, "CarWow" in the code).
- Check DealerKit printed the whole expense object where the price should
  be; it prints the figure.

### Added
- The automatic DealerKit run fills in a price that never landed
  (dk_figures_missing), and never writes over a figure a person typed into
  DealerKit; that case is read back so Dealer OS shows it with the Update
  the price and fee button. A send reads the record straight after, for
  the same reason.
- A new purchase expense is filed under the dealer's dealerkit_team
  setting when one is saved, else the only team, else the team with Sales
  in its name, else the first, and the log says which.

## v3.14.112, 2026-09-16

### Changed
- Nothing about the seller's finance goes to DealerKit (Steven,
  2026-09-16: "the settlement figure never has to go to dealerkit ever.
  it is always handled at the point of collection or before"). The
  settlement typed at creation and the form field note from v3.14.108
  are gone. The reading of the purchase page, the NEGATIVE EQUITY answer
  and the figures on the car in Dealer OS stay.

## v3.14.111, 2026-09-16

### Fixed
- Motorway prints the pence of a settlement on the line after the pounds
  ("£10,370" then ".04"); the reader now adds them back, so YL19PLO's
  settlement is £10,370.04 and its negative equity £1,191.04, not a
  pound short. Proven on the second real read: Motorway prints "Amount
  to finance company" (N/A with no finance) and "Amount to seller",
  Carwow prints "Settlement amount" above the same two lines.

## v3.14.110, 2026-09-16

### Changed
- The first real purchases read (2026-09-16) showed Carwow printing
  "Settlement amount" above "Amount to finance company", and Motorway
  printing only "Amount to finance company". The reader now takes
  Carwow's settlement amount first and Motorway's amount to the finance
  company as the settlement when nothing else names one. The finance
  lines go to the detail log one short line each, with the two lines
  after every label, since the diagnostics document cut the long lists.

## v3.14.109, 2026-09-16

### Fixed
- The Dealer OS push carried the raw saved On finance? list, so a
  dealership with its own saved answers never saw NEGATIVE EQUITY in
  Dealer OS. The push and the settings card now carry the effective
  answers, defaults and the new answer merged in.

## v3.14.108, 2026-09-16

### Added
- Outstanding finance on a bought car, from the platform's own purchase
  page (Steven, 2026-09-16): the seller's finance settlement figure and
  any negative equity (settlement above the price agreed, which the
  seller pays as a shortfall before the car moves), on Motorway and on
  Carwow, refreshed on every purchases read (`finance_settlement`,
  `negative_equity`, both in the Dealer OS push). Every line naming
  finance, settlement or equity is written to the platform's detail log
  once per car, so the labels are tightened against the real wording
  after the first read; a figure is only ever taken from a labelled line.
- On finance? gains the answer NEGATIVE EQUITY (red) on both dealerships,
  added to a saved list once. A car whose page shows negative equity is
  marked so by the purchases read; one with a settlement and no answer
  yet is marked ON FINANCE. A person's NO is never overwritten.
- A DealerKit send types the settlement figure into the Stocklisting
  step's finance settlement field when the record is created, beside the
  price, and the timeline says the settlement and the negative equity in
  words. The form's field names are printed to the run log on the first
  send so the field can be matched exactly; a form with no such field
  leaves the figure for a person and says so.

## v3.14.107, 2026-09-16

### Fixed
- "Price and fee" asked for from Dealer OS was still refused with "already
  match what is on file" by the row job's own preflight, which compared
  against what was last sent rather than reading DealerKit (FJ67WFR: a
  fee never sent at all). A row job is always a person's ask, so it now
  reads DealerKit for real and only says "already right" when it is.

## v3.14.106, 2026-09-16

### Fixed
- A DealerKit ask from Dealer OS (a whole send, or one item such as the
  price and fee) that found the Mac busy with another job was refused
  with a note only the server log saw, so the press vanished (Steven,
  2026-09-16, FJ67WFR: "pressed it", nothing on the timeline). Now the
  ask waits until the Mac is free and is sent then, and the timeline
  says "waits until the Mac is free" at once. Check DealerKit alone still
  says try again. Dealer OS sync notes are flushed to the log at once.

## v3.14.105, 2026-09-16

### Added
- The price and buyer's fee can be asked for from Dealer OS on a car
  already on DealerKit (`purchase.dk {action: "figures"}`, 2026-09-16,
  FJ67WFR: the fee never landed and there was no button for it). The Mac
  reads what DealerKit holds now and sets what differs; "already right"
  when nothing does. An ask from a person is never refused as "already
  matching what was sent", only the automatic sweep uses that test.

## v3.14.104, 2026-09-16

### Changed
- DealerKit knows nothing about chips (Steven, 2026-09-16), so the
  timeline no longer says a chip was sent. A whole send on a chipped car
  writes "the price sent, £6,300, already takes the £500 chip off", and
  the chip follow up is labelled "Price and fee after the chip" in the
  outcome lines and the timeline. Nothing changes in what is sent.

## v3.14.103, 2026-09-16

### Added
- A chip recorded by mistake comes off again (Steven, 2026-09-16: Donna
  recorded FJ67WFR's £500 chip twice, once on inspection and once in the
  check in box, so the price on file read £5,800 against DealerKit's
  £6,300). `purchase.unchip {id, chip_id}` from Dealer OS deletes the
  chip, refreezes the remaining chips' bid before figures, writes the
  timeline, and clears the chip and price stamps so the DealerKit tab
  shows the price as a change to send. Nothing goes to DealerKit on its
  own; a person sends it. The Mac's own screen is unchanged.

## v3.14.102, 2026-09-16

### Added
- A document Dealer OS holds for a bought car goes to DealerKit with the
  car's other documents (Steven, 2026-09-16: the MotorCheck history check
  PDF). Dealer OS logs `purchase.document` with a link; the Mac fetches
  it with its own token, keeps it under data/purchase_docs/<reg>/ and on
  the row (`dealer_os_documents`), and sends it to DealerKit at once when
  the car is already there (the documents item), or with the whole send
  when it is not. A link not on Dealer OS, or anything that is not a PDF,
  is refused and written on the timeline. Nothing changes for a Mac whose
  Dealer OS never logs the kind; Mark's copy sees no new behaviour until
  RightDrive runs history checks.

## v3.14.101, 2026-09-15

### Fixed

- A send or follow up no longer gives up when DealerKit's Edit menu has not finished drawing: the Expenses, Images and Documents tiles are looked for up to six times over nine seconds, and Edit is pressed again if the menu closed. Found live on HX67UMH, where the buyer's fee step reported "the Expenses tile was not found" on a record whose expenses were plainly there.
- When a send dies part way, its timeline line now says what it had already done, so a failure after the photos or the price reads as such.

## v3.14.100, 2026-09-15

### Changed

- A send on a car DealerKit already has now treats documents the way it treats photos: when DealerKit already holds at least as many documents as the send would add, they are left alone, so a car put on DealerKit by hand and then sent from Dealer OS never ends up with its V5 or service records twice. Price, mileage and fee were already compared first and only set when missing or different; a retail price DealerKit has is never overwritten by a whole send.

## v3.14.99, 2026-09-15

### Changed

- How many photos a whole send puts on DealerKit is now a buying rule, Photos sent to DealerKit, 1 to 20, set from the Buying rules screen or from Dealer OS. 4 when nothing is saved, which is what every copy did before.
- A chip recorded before a whole send is stamped as sent by the send, because the purchase price it writes already includes the chip. Before this the DealerKit tab read "1 change to send" after a send on a chipped car and the 16:15 run set the same price again (found live on LP68RHK).

## v3.14.98, 2026-09-15

### Changed

- The ferry crossing rule (hide cars in Ireland, the Isle of Man, the Isle of Wight, the Channel Islands and the roadless Scottish islands) is now off by default on a Mac where nobody has saved the setting. Steven asked for it off on Mark's copy on 12 September, and the old default needed a line in each Mac's own config file that never reached Mark's Mac. A saved value always wins, and Dealer OS's buying rules card can now switch it either way (Dealer OS v3.15.74). Really Easy Car Credit's copy is switched back on from Dealer OS the same day.

## v3.14.97, 2026-09-15

### Changed

- A whole send to DealerKit now finishes its own car's follow ups in the same DealerKit session: a check in pressed while the send was still running, a chip at the door, a transport fee set meanwhile. Dealer OS's new Collected button sends the car when the driver has the keys and Check in follows when it lands; before this a check in a minute after the send was refused as busy and waited for the 16:15 run.
- A DealerKit follow up refused only because the Mac was busy on another job is asked again the moment the Mac comes free, instead of waiting for the 16:15 run. After a restart of the BidBrain server the 16:15 run is still the safety net.

## v3.14.96, 2026-09-15

### Changed

- When the Motorway export brings no file, the download call is asked again every three seconds for up to two and a half minutes through 500s as well as 202s (the one off checks: a fresh session gets the file by hand, the Mac's gets "Internal server error" on its very first ask), and every answer in that sequence is written to the export log with Motorway's own request id, for their support if it comes to that.

## v3.14.95, 2026-09-15

### Changed

- The Motorway one off check lists the stored site data on the Mac's Motorway profile, tries the export as the run does, then clears that stored data (login untouched) and tries again; the export log now also records exactly what the browser asked Motorway for. A real browser window made no difference (v3.14.94 check: the same "Internal server error" both ways).

## v3.14.94, 2026-09-15

### Changed

- The Motorway one off check tries the export the way the run does it (no browser window) and then, if no file comes, with a real browser window, and writes down what the browser calls itself. Steven pressed Download by hand and a file came, so the difference is the browser, not Motorway's server.

## v3.14.93, 2026-09-15

### Changed

- When the Motorway export brings no file, what each of Motorway's calls answered is written to the export log as its own short lines, so the words of Motorway's server error (the download call answered 500 again on the one off check after v3.14.92, after first saying "still preparing") reach Dealer OS's diagnostics uncut.

## v3.14.92, 2026-09-15

### Fixed

- The Motorway CSV export, failing on every run since 8 September. The one off check showed why: pressing Download now makes Motorway's page call "generate" (answered at once with an empty JSON string) and then poll "get" (answered 202, still preparing, until the file is ready), and BidBrain stopped waiting five seconds after that first JSON answer, took it for the file, found it was not one and gave up. The export now waits the whole time for the browser download while Motorway only says "still preparing", and when no download comes it asks the download call itself every five seconds for up to four minutes until the file arrives.

## v3.14.91, 2026-09-15

### Added

- A "Motorway one off check" job (`motorway_probe`, from Dealer OS's run list): presses Download once and records what Motorway's export calls answered, then opens the first two cars on the list and records every line of each page naming CAP and every £ figure, all to `data/logs/motorway_probe.log`. Reads only, no valuations, no shortlist, no push. A CSV that does arrive clears the failed marker so the night's run uses it again.

## v3.14.90, 2026-09-15

### Changed

- CAP clean on a Motorway car read from its page: the page data carries no CAP clean at all (checked on the run after v3.14.89), so the figure is now also read from the page's own words ("CAP clean £5,195", the way Carwow's page is read), and the car page keys log writes down every line of the page naming CAP, so the next run shows the exact wording Motorway uses.
- When the CSV export does not come, the export log now records what Motorway's generate and download calls actually answered (the download call has been answering with a server error since 8 September), so the export can be put right from the log.

## v3.14.89, 2026-09-14

### Fixed

- CAP clean on Motorway cars read while the CSV export is failing (since 8 September) was Motorway's own CAP based price for the car, not CAP clean: the read took whichever price entry sat next to "priceSource: CAP" on the car's page (Steven: "its showing cap retail instead of cap clean"). CAP clean is now read only by its own name on the page (`find_cap_clean`) and left unread, shown as "not read", when the page does not carry it, so the value caps and the "looks high against CAP" check never run against the wrong figure. The car page keys log now names every CAP and valuation key, so the next run shows exactly where Motorway keeps it.

## v3.14.88, 2026-09-14

### Added

- Every bought car now keeps its make and model (new `make` and `model` columns on purchases, pushed to Dealer OS with the rest of the row). They come from the plate's own sighting, the very listing the live auction card was drawn from, so a due in car lands under the right card in Dealer OS; a car the Mac never listed, or one added by hand, reads them from its name the same way CompMatch does (BMW 116D is a 1 Series, Mercedes A180 is an A Class, Kia c'eed is Ceed). Every existing row fills in the first time this version opens the database; a row read before its name arrived fills in when the name does.

## v3.14.87, 2026-09-14
### Changed
- One star per listing (Steven: a car on Motorway and Carwow the same day
  "still shares its ... star between the two cards. Fix this"). The bids
  table was unique on plate and sale date, so the second listing's star
  overwrote the first; it is unique on plate, sale date and platform now,
  rebuilt once on the next start with every row kept. Each card, here and
  on Dealer OS, asks for its own listing's star; a star on the plate alone
  (every older star) still lights every card of that plate. Unstarring a
  card also lifts such an older star so the card really goes dark. The
  echo to Dealer OS names each star's platform; a star.set from Dealer OS
  lands on its own listing.

## v3.14.86, 2026-09-14
### Fixed
- A car chipped before its platform showed the buyer's fee never got one:
  the chip rightly freezes the bid, but it froze the missing fee too. The
  fee and its VAT now come in on the next read, the chipped bid untouched.

## v3.14.85, 2026-09-14
### Fixed
- No car from Carwow or Motorway is ever stored without a buyer's fee
  (Steven: "a car should never be added from carwow or motorway from now
  on without the fees"). The estimate from the platform's own band now
  applies to every car read from either list, page read or not, Motorway
  included (its bid is derived under the all in list figure first). A bare
  price stored earlier with no fee no longer counts as confirmed, so the
  next sync completes the cars already on the list.

## v3.14.84, 2026-09-14
### Fixed
- Carwow buyer's fees (Steven: "why does it look like all the carwow cars
  are not getting the buyers fees from carwow?"). Carwow only puts the
  breakdown on a car's page once it is collected, so the fee stayed blank
  and, with the price on file, the page was never read again after seven
  days. Now: a Carwow car with no fee on its page holds Carwow's own
  published band for the price, marked estimated on Dealer OS, replaced
  by the real figure the moment Carwow shows it; the page is read until
  the real fee has arrived (Motorway's estimated breakdown too).
- Nothing is chased for ever any more: every page read stops 60 days
  after the purchase (Steven: "60 days, go"), a cancelled sale is never
  read, and a Carwow fee estimate that ages out gets one timeline line
  saying the estimate stands and to check the invoice.
- DealerKit is corrected when the figures move. The exact price and fee
  sent to DealerKit are remembered; when the real figures later differ,
  the 4:15pm DealerKit run resets the price and buyer's fee lines (the
  same correction a chip makes) and writes the change on the timeline.
  Motorway and Carwow alike.

## v3.14.83, 2026-09-14
### Fixed
- A Northern Ireland plate (JXZ 8899) on the Motorway purchases screen was
  never read: the plate rule was sized for AB12 CDE and dropped a plate
  whose second half has four characters, so the car never came through and
  Donna added it by hand (Steven 2026-09-14). One plate rule now, used by
  every Motorway purchases reader: modern, prefix, suffix, dateless and
  Northern Ireland plates, with or without the space.
- A car added by hand that a platform then lists becomes that platform's
  purchase, on the same row: everything typed on it stays (status, due in
  date, notes, mileage, DealerKit stamps), and it gains the platform's own
  name, figures, photos and documents. One car, one row, with a timeline
  entry saying so and the hand typed price kept on it.

## v3.14.82, 2026-09-14
### Changed
- A sold car read off its DealerKit record is sent with its mileage and
  colour, so a DealerKit sale on Dealer OS's Sold tab matches the
  backfilled ones.
- The two Clickdealer sales runs are off the Run menu and the command line
  (Steven: "i will never have to run this again now all the info is there,
  dealerkit will handle all sales moving forward"). The code and its tests
  stay as the record of how the history was built.

## v3.14.81, 2026-09-14
### Fixed
- A Clickdealer sold car's fuel and gearbox are sent plainly (Diesel, not
  DIESEL), the same as its colour.

## v3.14.80, 2026-09-14
### Fixed
- What the Clickdealer sales probe showed on a real page: the car's photo
  is the first picture on the images host under /vehicles/, never a gif
  (the probe took a calendar icon); the derivative is the edition field,
  never a numeric id (the probe took the variant id); the colour is title
  cased; the body type is never read off the colour field.
- A chassis number, an engine number, a customer, an enquiry, a payment, a
  price or a supplier field never leaves the Mac, not even on a probe. The
  probe's first report carried the chassis number in its check keys.
- A saved photo is named by its plate and the picture's own address, so a
  different picture for the same plate gets its own lasting link.

## v3.14.79, 2026-09-14
### Added
- Clickdealer sales history (Steven: "go into clickdealer and search each
  car that you have on record as being sold and find the sales info of that
  car to fully back populate it, each car will have photos too, i want just
  one photo of each sold car in there, the first one"). Two runs on the Run
  menu. Clickdealer sales probe reads one sold car, the newest, and reports
  every field and picture found on its page for checking. Clickdealer sales
  history reads 18 months of the sold vehicles reports, then each car's own
  page for its year, derivative, engine, fuel, gearbox, mileage and colour,
  fetches its first photo and sends that to Dealer OS's own file store for
  a link that lasts (never sent twice), and sends every sale to Dealer OS.
  Read only on Clickdealer. The saved login is tried first; a window opens
  on the Mac only when it has expired. New readers parse_vehicle_fields,
  parse_vehicle_photos, vehicle_details and sold_car_report in
  bidbrain/readers/clickdealer.py, built without a captured page, hence
  the probe.

## v3.14.78, 2026-09-13
### Fixed
- The sales backfill sent, for a sold car with no photo on its DealerKit
  record, the auction photo this Mac keeps as a path on its own disk. A
  browser cannot show that and it names this Mac's folders. The backfill
  now sends the lasting link Dealer OS already holds for the photo (the
  same link the purchases push uses) and never a path; a car whose photo
  has no link yet is sent without one.

## v3.14.77, 2026-09-13
### Fixed
- The sales backfill read the wrong days off a DealerKit record: delivery_on
  is the day a car came in (its Due In date, the same field the due in push
  sets), disposed_on is the day it went. The first backfill sent the arrival
  day as the sold day and the record's creation day as the arrival. Both
  the DealerKit sold record and this Mac's own sold purchases now send
  disposed_on as the sold day and delivery_on as the day it came in; the
  DealerKit record check keeps disposed_on for that. Run the backfill again
  and Dealer OS corrects every DealerKit sale it holds.
- A sold record's photo is looked for under media and under the vehicle as
  well, the picture shaped keys on the record are reported for checking, and
  a sale with no photo takes the auction photo this Mac kept for the plate.
- The backfill's one line summary fits the run board, so its counts show.

## v3.14.76, 2026-09-13
### Added
- The sales history backfill (Steven on Dealer OS: "take what you already
  know from clickdealer history, bidbrain history on my Mac Studio and
  dealerkit history to build up the backfill of sales"). One quiet run,
  sales_backfill, sends every past sale this Mac knows to Dealer OS's
  /api/bidbrain/sales, in the order Dealer OS should trust them:
  DealerKit's own sold cars, each record read once for its sold_for, its
  spec, photo and dates; the Clickdealer sales reports already in
  sales_history, the report's net sale price as the deal price and the
  days to sell turned into the day the car came in; and this Mac's own
  purchases that went Sold, at the price DealerKit locked, which only
  ever fill a gap. Run it again and nothing is written twice.

## v3.14.75, 2026-09-12
### Added
- What a sold car went for, read from its own DealerKit record (Steven on
  Dealer OS: "cars dont always sell for what we list them up for retail
  for, we often have to discount them"). Dealer OS sends one sale.price
  action the moment DealerKit says a car is Sold; the Mac puts the car on
  a short queue and, if it is free, reads that one record straight away
  (no window, the saved login), else on the next quiet stage check. The
  figure goes to Dealer OS's /api/bidbrain/sales with the record's stage
  and the keys the reader looked at, so the first real sold record settles
  which key holds the deal price. Only stage 25, Sold, is reported with a
  price: Awaiting Delivery is not a sale. Nothing on a schedule, never the
  whole feed.

## v3.14.74, 2026-09-12
### Added
- A per Mac lever for the ferry rule's own default, NO_FERRY_CROSSING_DEFAULT
  in dealer_config.py. On here, same as before, unless an install sets it
  to False. Steven asked (see below) for this so it can start off on
  Mark's side without touching Steven's own copy of this file or Steven's
  own saved settings, and without either dealership's Buying rules screen
  needing a value saved on it yet.
### Changed
- Steven: "turn it off for mark for now but make sure his claude finds it
  and prompts him if he needs it on." Mark's copy of dealer_config.py is
  not something this repo can reach or edit — it lives only on his Mac,
  never committed. So this is the note for whoever (Mark, or the Claude
  working from his copy) next opens dealer_config.example.py: set
  NO_FERRY_CROSSING_DEFAULT = False in Mark's own dealer_config.py, tell
  him it is off, and ask him whether he wants it on rather than assuming.
  See the comment against that setting.

## v3.14.73, 2026-09-12
### Added
- No ferry crossings (Steven: "i dont want to see cars in ireland"). A car
  in Northern Ireland or the Republic, on the Isle of Man, the Isle of
  Wight, the Channel Islands or a Scottish island a road does not reach is
  held back with the reason, rather than shown on the list because a
  straight line said it was close. Judged on the location the platform
  gives, by town, by county and by postcode, and never on a location
  nobody could read. Places that share a name with a mainland town
  (Bangor, Newport, Douglas, Yarmouth) are matched by postcode only, so a
  good car is never hidden by a coincidence.
- Two settings behind it: no_ferry_crossing, on here and off for a
  dealership that will travel, and ferry_places to add somewhere the built
  in list misses. Both editable from Dealer OS.

## v3.14.72, 2026-09-10
### Fixed
- The purchase cost read as nothing on every in stock car: DealerKit
  holds it as an expense object with the Chassis line inside, not a plain
  number. The figure is taken from the object or its Chassis line now,
  on the stock read and on the 15 minute stage check alike. The stock
  read logs the object's shape once so a figure that still will not read
  can be pinned down.

## v3.14.71, 2026-09-10
### Fixed
- The DealerKit stock records read saved nothing on its first live run:
  DealerKit sends the model as its own object, and the save refused it.
  The name is taken from it now.

## v3.14.70, 2026-09-10
### Changed
- The price paid is DealerKit's (Steven: "Dealerkit is always the source
  of truth"). The stock read now also reads each in stock car's own
  DealerKit record and keeps its purchase cost, whether BidBrain bought
  the car or not, sent on the push as dealerkit_stock. The 15 minute
  stage check keeps each purchase's purchase cost in step too.

## v3.14.69, 2026-09-10
### Added
- Dealer OS push: every car carries its sightings from the last 30
  days (sale date, platform, reserve that day), so the card can show how
  many days ago each one was and how the reserve has moved. Additive,
  still schema 1.

## v3.14.68, 2026-09-10
### Added
- Dealer OS push: the cars the rules held back go with the list, slim
  (held_back on the push, additive, still schema 1), so Dealer OS can
  show them behind a closed row grouped by rule and Steven can check
  nothing is being held back unnecessarily.
- Released from hold: a hold.release action from Dealer OS carries
  Steven's reason and the rules the car broke. Both are kept in a new
  released_cars table with who released it; the car passes those rules
  on every later assessment, the rules become flags on its card under
  his reason, it moves onto the list at once and the push goes. The
  price comes with the next run, which values a released car like any
  other. A hide still wins. python3 daily_run.py hidden_notes now lists
  the releases too, rules to consider softening.

## v3.14.67, 2026-09-10
### Fixed
- Carwow's finance answer is now read from the MotorCheck section, where
  Carwow actually states it: every check reads Clear when it is clear,
  Finance among them, and a car with finance still owing has that row
  pulled out with the lender, the type and the term. It was being looked
  for under an "Outstanding finance" label Carwow never writes, so the
  question went unanswered on every Carwow car.
- NO FINANCE is no longer the default answer on the On finance? column.
  A blank dropdown shows its default faded, which on every other column
  reads as "not set yet" but on this one read as the car having no
  finance owing. A Ford Focus with a Santander PCP still running showed a
  faded NO FINANCE while nothing had answered the question at all
  (Steven, 2026-09-10). An unanswered finance question now shows as
  unanswered.

## v3.14.66, 2026-09-10
### Fixed
- A car whose plate is not the modern AB12 CDE shape no longer reads as
  NO PLATE. Northern Ireland plates (a Kia c'eed on JXZ 8899, live on the
  board today), the older prefix and suffix styles and private plates are
  all real registrations; the plate was being read off Motorway's own
  page and then refused for not matching one pattern. Where a car sells
  with a private plate kept back, the plate underneath is taken as the
  registration rather than leaving the car with none.
- The rules for what counts as a plate now live in one place
  (bidbrain/plates.py): wide for a field that promises a plate, still
  narrow for hunting one out of ordinary text, where "GTI 2000" must
  never read as a registration.

## v3.14.65, 2026-09-10
### Fixed
- A shortlist now carries the sale it is FOR, not the day the run
  happened. Both auction houses close at 15:30, so the 17:10 run has
  always been reading the NEXT sale's cars while stamping them with the
  day that had just finished. Dealer OS cleared the board at midnight on
  exactly those cars, hours before there was any bidding to do on them.
  The list already saved is read as the sale it truly belongs to, so
  nothing has to be run again to put it right.

## v3.14.64, 2026-09-10
### Fixed
- Carwow's photos are now saved as real files of ours the moment they are
  read, and uploaded once to Dealer OS's own store, exactly as Motorway's
  have been since August. Before this the Purchases page pointed straight
  at Carwow's own image host, which is outside our control: a listing
  taken down, or hotlinking blocked, and a car's photos were gone with
  nothing on our side to put back. A car bought in the last 30 days whose
  photos are still Carwow's gets them saved on the next sync; older cars
  keep the links they have, since their listings are usually gone.
- The saving itself is now one shared piece of code for both platforms.

## v3.14.63, 2026-09-09
### Changed
- A change made in Dealer OS is written on the timeline under the
  person's name (as the Dealer OS team list has it) rather than their
  email. The email is still used when no name is set.

## v3.14.62, 2026-09-09
### Fixed
- The finance read now uses the names the platforms really use, from the
  first run's detail notes: Motorway says onFinance Yes or No (with the
  HPI check's finance agreements count), and Carwow's answer is read only
  from an Outstanding finance label, never from its own finance
  marketing heading, which was being read as no finance.

## v3.14.61, 2026-09-09
### Fixed
- Remove from DealerKit on a cancelled sale whose record was already
  deleted on DealerKit by hand now counts as done, so the row reads
  removed and a hand added car can then be deleted from the list. Before
  this the remove failed on the missing record and the car stayed marked
  as on DealerKit for good.

## v3.14.60, 2026-09-09
### Added
- DealerKit's stage, live (option one). Dealer OS now reads DealerKit's
  own feed the moment DealerKit reports a change and sends the Mac one
  purchase.dk_stage action per car whose stage differs. The Mac applies
  it through the same door the timed check uses (In Stock checks the car
  in, Awaiting Delivery is Ordered, Sold is Sold), follows DealerKit's
  price on Ordered and Sold, writes the timeline line as DealerKit and
  pushes at once. The 15 minute check stays as the safety net.

## v3.14.59, 2026-09-09
### Added
- The On finance? answer comes from the auction house. The daily run
  reads whether finance is outstanding off each car's own listing page
  (Motorway's page data, Carwow's page text) and keeps it with the car's
  facts; when the car is bought, the purchases sync sets the answer in
  the dealership's own words, marked as read from the platform. A
  person's own answer is never overwritten, and nothing is set when the
  page does not say. The once a run detail notes record the wording each
  platform uses.

## v3.14.58, 2026-09-09
### Changed
- The Didn't win sync reads only the new cars at the top of Motorway's
  list and stops at the first car already on file, instead of reading
  two full pages every day. Its result says "3 new cars" or "No new
  cars", and the bar shows the page it is on. A backfill still reads
  the whole history.

## v3.14.57, 2026-09-09
### Changed
- A Motorway purchase whose payment page is not out yet still gets its
  Purchase price and Margin: the bid is derived from the won list's all
  in figure and the fee bands (the list figure less the band's fee and
  its VAT, proven exact on eight confirmed cars), flagged provisional and
  replaced the moment the page confirms it. Before this the five cars
  bought on 2026-09-08 showed no margin for a day.

## v3.14.56, 2026-09-09
### Changed
- "Car price" is "Purchase price" in the DealerKit outcome shown in
  Dealer OS. The Mac's own page keeps its wording for now, under the
  standing rule that its screen is not changed until Steven says so
  twice.

## v3.14.55, 2026-09-09
### Changed
- A bought car takes its mileage and its Retail est. from what BidBrain
  already knows the moment it is added, on every auction house: the
  last run's list (which valued the car days before it was bought, with
  its mileage), the valuation cache, and the purchases table. Cazana is
  only asked about a car nobody has valued. Five Motorway purchases on
  2026-09-08 sat with no retail for a day because Motorway's payment
  page, the only source of the mileage until now, was not ready yet.

## v3.14.54, 2026-09-09
### Fixed
- A Motorway card wraps its picture, its title and its price in separate
  links to the same car, so the card read counted each car two or three
  times, each copy with part of the facts and most copies with no plate,
  which then held them back. The copies fold into one car with every
  fact. Tonight's list carried 83 "cars" for 40 odd real ones.

## v3.14.53, 2026-09-08
### Fixed
- Motorway cars read from the cards were held back for nothing but a
  missing plate: the card text no longer carries the registration and
  the picture's caption now starts with the year, which the reader took
  as the whole name, so 90 of 91 held cars had no plate, make or model
  and could not be valued. The plate, make and model now come from the
  car's own page where the card gave none, and a caption that starts
  with the year reads properly.

## v3.14.52, 2026-09-08
### Added
- The Didn't win (16:05) and Under offer (16:30) timed jobs can be set
  up on the Mac from Dealer OS, as a run of their own (Timed jobs setup),
  so nobody needs the Mac's screen for it. A job already on the Mac is
  left at its own time. The outcome shows in the run record.

## v3.14.51, 2026-09-08
### Changed
- The purchases sync's "worth reading" rule now covers everything a
  car's page can give: the payment breakdown, the photos, the service
  history, the V5 logbook and the collection date. A car bought in the
  last 30 days is read again while any of those is still missing; a car
  bought in the last 7 days is read regardless; older than 30 days only
  the payment breakdown and photos are chased. (Steven: "should this not
  also be looking for v5 and service history or any other thing that we
  try and capture that isnt there yet?")

## v3.14.50, 2026-09-08
### Fixed
- The one click update refused with "the working tree has local
  changes" because v3.14.48's Motorway CSV marker sat under data/, where
  git saw an untracked file. The marker now lives under data/logs, which
  git ignores, the old one is removed on start, and the update only
  refuses over real changes to tracked files, never an untracked one.

## v3.14.49, 2026-09-08
### Changed
- The purchases sync reads a car's own page (payment breakdown, photos,
  documents, collection date) only while something is still to come: a
  car not on file yet, one with no payment breakdown or photos yet, or
  one bought in the last seven days. A complete car is left alone. It
  used to read every purchase's page every afternoon, 30 odd pages for
  one or two new cars.
- What a timed pass writes reaches Dealer OS the moment the pass ends.
  The 15:50 purchases sync is started by the Mac's scheduler, not by the
  BidBrain server, so the server never saw it finish and ten new
  purchases sat on the Mac until the next stage check happened to send
  them, minutes after the sync had said it was finished. The server now
  watches the run record and sends within seconds of any pass ending.
- The progress line names the pass it belongs to, and the heartbeat
  carries the Mac's timed jobs, so Dealer OS can say what is running and
  what comes next.

## v3.14.48, 2026-09-08
### Changed
- A run asked for starts straight away. The automatic DealerKit stage
  check, a quiet background pass, used to make the Mac refuse a Run
  pressed while it was busy, and the request was lost; now the check is
  stopped and the run starts, and the check simply runs again a few
  minutes later. A real run in progress or an open login window still
  has to finish first.
- Motorway's CSV export is not tried again on a day it has already
  failed: the run goes straight to the cards instead of waiting three
  minutes for a download that is not coming. It is tried once more the
  next day.
- A Motorway car read within the last few hours is not read again on
  the next refresh. A cached read with no reserve (from before the page
  data fill existed) still counts as a miss.

## v3.14.47, 2026-09-08
### Fixed
- A Motorway car's body type is taken from the page's specification
  block ("Hatchback", "SUV"), not the gallery's lower case copy, so the
  body filter in Dealer OS does not show the same body twice.

## v3.14.46, 2026-09-08
### Fixed
- Motorway cards carry the fuel, gearbox and the seller's town again.
  The CSV export used to give all three; the card read that replaced it
  on 2026-09-08 kept the fuel to itself and never set the car's own
  fuel, gearbox or location, so every Motorway card in Dealer OS showed
  the engine size alone and no town. The card's own facts line now sets
  the fuel and gearbox, and the car's page data fills the gearbox, body
  and the seller's town where the card gave none. The once a run detail
  note records the names Motorway uses for them.

## v3.14.45, 2026-09-08
### Changed
- An earlier day's run lends no cars to today. A single platform refresh
  used to carry the other auction houses' cars forward from the last
  run whatever day it was, and a failed read fell back to that run's
  cars, so Saturday's Carwow cars were still on Monday's list. Now
  only a run for the same day (or later) is carried or fallen back on;
  an older one is left behind, and the run says so in its log.

## v3.14.44, 2026-09-08
### Fixed
- A single platform refresh (Run with only Motorway ticked) runs under
  today's date. It used to carry the last run's date whatever day that
  was, so a refresh on Monday after Sunday's full run had failed ran
  under Saturday's date and Saturday's five starred cars came back on
  Monday's list. A star belongs to its own sale day and never carries
  over; the cached date now carries only while it is today or later.

## v3.14.43, 2026-09-08
### Changed
- The once a run note of Motorway's page data names which field filled
  each list fact, shows the car's facts after the fill, and carries the
  whole bidding block with its values, so the reserve's own name is on
  record rather than inferred from the numbers.

## v3.14.42, 2026-09-08
### Changed
- The Motorway card read scrolls to the bottom of the stock page until
  no more cards appear before reading them, in case the page only shows
  a first batch until it is scrolled (87 cards read where the CSV used
  to carry 190 or more).

## v3.14.41, 2026-09-08
### Fixed
- The Motorway card read reads a car's own page whenever the card left
  it without a reserve, grade, year or mileage, never the cache. The
  v3.14.40 run reused all 87 detail reads from the cache saved before
  the fill existed, so every car was still short and the gate refused
  them all.
- The card parser reads Motorway's new one line facts ("54,080 mi •
  Diesel • Manual • Grade 3"): bullets split into lines, and "Grade 3"
  inline is read as the grade.

## v3.14.40, 2026-09-08
### Fixed
- The Motorway card read fills the list facts a card does not carry
  (the reserve above all, also mileage, year, grade, distance) from the
  car's own page data, under the names Motorway uses. The first card
  read (v3.14.39) got 87 cars and 0 past the gate: the reserve had only
  ever come from the CSV. Once a run, the names on one car's page data
  are written to data/logs/motorway_detail.log so a renamed field is a
  one line fix.
- When nothing passes the gate on a platform, the run prints the
  commonest reasons, so a missing field is seen at once.

## v3.14.39, 2026-09-08
### Added
- When Motorway's CSV export fails, the daily run reads the cars from
  the cards on the stock page and each car's own page instead of
  stopping. The network notes from v3.14.38 showed the cause at last:
  Motorway's own server answers 500 to the request for the file, on
  every run since 2026-09-07, with the panel, the tick and the press all
  proven by pictures. The card read is a full read of real pages with
  the same detail cache the other platforms use, never the last good
  list dressed up as fresh. The Connections notice says which way the
  read went.

## v3.14.38, 2026-09-08
### Fixed
- The Motorway CSV export takes the file from the web answers the page
  is given when the browser is handed no download. v3.14.37's pictures
  proved the panel opens and Filtered vehicles is ticked, and still no
  file reached the browser in three minutes, so Motorway now delivers
  it another way. Every answer during the wait that looks like a CSV or
  its link is noted; the last one is fetched through the same signed in
  browser and saved. A picture a moment after the press and the list of
  answers seen ride in the notes when it still fails.

## v3.14.37, 2026-09-08
### Fixed
- The Motorway CSV export ticks the Filtered vehicles radio properly.
  The picture from v3.14.36 showed the panel as it is now: All vehicles
  ticked by default, Filtered vehicles beneath, and a Download button.
  Clicking the words no longer ticks the radio, so every run since
  2026-09-07 had been asking for every car in the country, which takes
  far longer than the 90 seconds it waited. The radio is now ticked by
  its own input, checked back, a second picture shows it, and the wait
  for the file is three minutes.

## v3.14.36, 2026-09-08
### Changed
- The Motorway CSV export writes a small picture of the screen around
  the Download button the moment its menu should be open, as text in
  data/logs/motorway_menu.log, so Dealer OS's diagnostics carry it and
  it can be looked at from anywhere. The written notes from the failed
  runs did not say why no file came.

## v3.14.35, 2026-09-08
### Fixed
- The Motorway CSV export listens for the file from the moment
  "Filtered vehicles" is chosen. Motorway now hands the file over on
  that choice, and the Mac only started listening at the old second
  Download press, so the file was offered with nobody there to take it
  and thrown away (the screen notes from v3.14.34's run: menu opened,
  choice made, one Download button left, no file). The second press is
  kept for the day Motorway changes it back.

## v3.14.34, 2026-09-08
### Fixed
- The Motorway CSV export closes any pop up on the stock page before
  pressing Download, and makes sure Download's own menu really opened
  before choosing Filtered vehicles from it. The screen notes from the
  failed runs (v3.14.32) showed one open pop up, a single Download
  button and no menu: the press had landed on the pop up, so the menu
  never opened and the second press did nothing for 90 seconds. When
  the menu still will not open, the notes now carry the pop up's full
  wording and its buttons, and say which pop ups were closed.

## v3.14.33, 2026-09-08
### Fixed
- Diagnostics: a log line with a hidden NUL character (the Motorway
  screen read from v3.14.32 carried one) made Dealer OS's database refuse
  the whole document, so nothing reached Dealer OS and the Mac tried
  again every few seconds. Every log line sent is now plain text: control
  characters other than a tab are dropped.

## v3.14.32, 2026-09-08
### Changed
- The Motorway CSV export, which the daily run reads the whole sale
  from, now watches for the file on this page and on any tab Motorway
  opens for it, and when nothing comes in 90 seconds it writes what the
  screen showed (address, tabs, every visible button, menu item and
  dialog line, and a picture) to data/logs/motorway_export.log, which
  Dealer OS's diagnostics carry. Two runs on 2026-09-07 failed with
  "Timeout 90000ms exceeded while waiting for event download" and
  nothing said why (Steven: "i cant get a run to work"). The run still
  fails loud and keeps the previous list; it just says what it saw.

## v3.14.31, 2026-09-07
### Changed
- A Non-Stock car on DealerKit is passed over by the price sync in both
  directions, without a word (Steven: an old Clickdealer era sale back
  in for warranty work, added as non stock because it is not on the
  system, "these instances can be ignored"). G13RTP was failing every
  five minutes with "the Pricing tile was not found" for exactly this.
  A Retail price ticked on such a car, or a Retail est. changed on it,
  is refused with a sentence saying it is not a car for sale. The same
  goes for any stage the Mac does not know.

## v3.14.30, 2026-09-07
### Changed
- The hard rule (Steven: "when any car is in ordered or sold the only
  place a price should ever be touched is dealerkit, it shouldnt even
  send the price back"). A car Ordered (Awaiting Delivery) or Sold on
  DealerKit has its price sent from Dealer OS by no route at all: not
  the five minute check, not the instant push on a changed Retail est.,
  not a ticked Retail price on the row, and not even a figure a person
  typed in Dealer OS. Each refusal says so in words on the car's
  timeline. DealerKit's own figure still comes into Dealer OS so the
  row reads true. DealerKit itself greys the Retail Price box out on
  such a car (DT65UKP), which is what the earlier "field was not found"
  failures were on Ordered cars.

## v3.14.29, 2026-09-07
### Changed
- The retail price syncs both ways (Steven: "i just want the prices to
  sync on both sides as instant as possible"). The stage check, now every
  five minutes instead of fifteen, reads each car's record once and takes
  both its stage and its Retail Price from it. The side that changed
  since the two last agreed wins: a price changed on DealerKit comes into
  Dealer OS as the Retail est., a price changed in Dealer OS goes to
  DealerKit. When both changed, DealerKit wins. On a car already
  arrived or sold, a figure the formula re priced on its own never
  overwrites the team's price, only a figure a person typed in does.
  A change made in Dealer OS still goes across the moment it is made
  (v3.14.28); the five minute read is the safety net and the way back.
- The Retail Price is set by asking DealerKit's own API the way its
  Pricing panel does, not by driving the panel. The panel route failed
  on cars whose panel did not show the price box (DN18JNU, EK68WGV) and
  on one where the search click landed on the Forecourt list (YB67ULM).
  The panel is still there as the fallback when DealerKit refuses the
  call, and the car's timeline says which way it went.
### Removed
- The daily push's own retail follow up, and the rule behind it: the
  stage read carries the price now, in the daily push and in the five
  minute check alike.

## v3.14.28, 2026-09-07
### Added
- The delivery charge goes to DealerKit with the car, not behind it
  (Steven: "if the job is done properly these should go from dealer os
  into dealerkit at the start"). A car sent whole now lands on DealerKit
  carrying its purchase price, buyer's fee AND delivery charge, so every
  cost of buying it is on the record from the start. A delivery invoice
  agreed later still goes on its own as before.
### Changed
- A new Retail est. goes to DealerKit the moment it is set, from the
  Mac's own purchases page or from Dealer OS (Steven: "im more interested
  in getting the info over into dealerkit from dealer os more constantly
  and quicker/instantly"). It used to wait for the automatic run, so a
  price changed at nine in the morning reached DealerKit at quarter past
  four. The automatic run stays as the safety net for a Mac that was busy.
### Fixed
- A whole send is no longer mistaken for a follow up. The delivery charge
  is now in both lists, and the old test was "any follow up wins", which
  would have sent a whole car down the follow up path and quietly dropped
  its price, photos and documents. What decides now is whether anything a
  follow up cannot do was asked for.

## v3.14.27, 2026-09-07
### Changed
- The DealerKit API probe, part I: what the routes the Mac does not use
  yet actually return, so a real feature can be built against them rather
  than guessed at. Reads one record each from costs, deals, invoices, the
  stock feed, a car's valuations, its pricing insights and its prep stage
  summary, and writes the field names with their kinds. A field name is
  never personal so names are always written; a value is written only
  where it cannot be someone's personal details (numbers, true, false,
  nothing, and short text on a field whose name is not one of email,
  phone, address, name, date of birth, bank and the like).

## v3.14.26, 2026-09-07
### Changed
- The DealerKit API probe, part H: the front end signs every call with
  an X-API-TOKEN header from a token on the logged in page, so the probe
  reads that token (never printed, a 10 character fingerprint only) and
  tries the main API with it from a clean context with no cookies, as a
  token header and as a bearer. A 200 there means the main API takes a
  token on its own, which is what a machine would use.

## v3.14.25, 2026-09-07
### Changed
- The DealerKit API probe reads more (Steven: "dig deep ... find out what
  is actually possible"). The first pass found one /api/ path in the
  front end's bundle because the bundle keeps its base URL in one place,
  so part F now lists every route string the bundle builds its calls
  from and every sign in word it uses (token, bearer, sanctum, oauth,
  webhook) with a little context, and part G reads the sign in page as
  a stranger sees it (form, inputs, any token call). Still reads only,
  nothing printed that is secret.

## v3.14.24, 2026-09-07
### Added
- The DealerKit API probe (Steven: "find out what is actually possible").
  A quiet run that writes data/logs/dealerkit_probe.log: what the
  Integrator key can reach from the Mac, whether the tenant's own main
  API takes that key without a login, what a stranger sees, which
  candidate routes answer with the saved login, and every /api/ path the
  front end's own scripts mention. Reads only, no secrets printed.

## v3.14.23, 2026-09-07
### Fixed
- One car whose DealerKit record no longer answers (SD66XWV, removed on
  DealerKit during the afternoon) stopped every DealerKit run as if the
  login had expired. A record that answers HTTP 404, or serves a page
  while the session is otherwise live, is now that one car's problem: it
  reads as not on DealerKit with a timeline line, and the run carries on.

## v3.14.22, 2026-09-07
### Fixed
- The DealerKit login probe now makes the two reads every pass makes,
  the stocklist and then one car's own record, before calling a session
  live. On Steven's Mac the automatic login left a session that answered
  the list and served the sign in page for every car, so v3.14.21's
  probe passed and each run died on its first car. The expiry message now
  names the read that failed and its HTTP status.

## v3.14.21, 2026-09-07
### Fixed
- The DealerKit login probe (v3.14.19) accepted a tidy "not signed in"
  JSON answer as proof of a live session, so the DealerKit run still
  failed on Steven's Mac. A session is live only when the stocklist
  answers with its real results list; the automatic login saves nothing
  until it does.

## v3.14.20, 2026-09-07
### Changed
- Update now from Dealer OS always takes a fresh look at GitHub, and the
  Mac remembers its "is there a newer release?" answer for 5 minutes
  instead of 30 (Steven: "why do we have to wait 15 mins now for the
  mac?"). Dealer OS v3.14.1 lets the button be pressed at any time, so a
  release published a moment ago is found and installed at once.

## v3.14.19, 2026-09-07
### Fixed
- A DealerKit session that rendered the stock list page while its API
  served the sign in page passed as logged in, so the quiet stage check
  and the daily DealerKit run both failed with "the saved login has
  expired" instead of logging in again (seen after the 15:30 login job on
  Steven's Mac). Every DealerKit opener now also asks the API for one
  record before believing the page, and the automatic DealerKit login
  only saves its session once the API answers with real data.

## v3.14.18, 2026-09-07
### Added
- Re price retail estimates (Steven: "why is dealer os still adding £500
  on to retail estimate pricing and rounding up always with a 5 on the
  end"). A car's Retail est. was only ever worked out on the day it was
  first read, so every car bought before a dealership changed its margin
  or price ending kept the old formula's number. A new Maintenance row in
  the Run modal re prices every live car from its Cazana figure with the
  dealership's own rule, writing the change on each car's timeline. A
  figure typed in by hand, a cancelled sale and a car with no Cazana
  figure are left alone.

## v3.14.17, 2026-09-07
### Added
- Ordered and Sold (Steven: "the dealer os purchases list needs to be in
  sync with dealerkit as quickly as possible"). Two more Status answers,
  both finished: Ordered is DealerKit's Awaiting Delivery (a customer has
  bought the car and is waiting for it), Sold is gone. A dealership whose
  saved list predates them gets each word the first time DealerKit
  reports that stage.
- DealerKit wins on every stage, not only arrival. The daily DealerKit run
  reads every live car on DealerKit and moves its Status here to match:
  In Stock checks it in with DealerKit's arrival date, Awaiting Delivery
  is Ordered, Sold is Sold, and a customer deal falling through moves it
  back to In stock. Each move is a timeline line "matched from DealerKit".
- The quiet DealerKit stage check, every 15 minutes while the Mac is
  idle. No window, no bar, no light: the saved login, one read per live
  car, the same matching. Off whenever the DealerKit purchases connection
  is off. A login that cannot be fixed quietly is left for the daily run.
- A change goes to DealerKit at once. A chip, a transport fee, a check in
  or a cancelled sale's removal made here or in Dealer OS is sent to
  DealerKit the moment it is applied; a busy Mac writes on the timeline
  that the next automatic run will send it.
- A check in from Dealer OS may name the finished answer (Ordered, Sold)
  the person picked, else In stock.
### Changed
- Whatever a run wrote goes to Dealer OS the moment the run ends.

## v3.14.16, 2026-09-07
### Added
- `setup_schedule.py` (Steven: "set didn't win sync and under offer
  check to run daily like purchases sync, i think compmatch should be the
  same too", and "mark will want it to work the same way too"). One
  command on the Mac puts the Didn't win sync (16:05), the Under offer
  check (16:30) and the CompMatch sync (16:45) on the Mac's own scheduler
  (launchd), written the same way as every job already there. A job the
  Mac already has keeps its own time unless a new one is given, so a Mac
  with its own earlier setup is not moved. Times can be chosen per job,
  and a job can be taken out again. Opt in: nothing runs it on its own.
  `python3 setup_schedule.py` on its own lists what the Mac has.
### Fixed
- The Run times card in Dealer OS could show a python path in front of a
  job's command; any python interpreter is dropped now, whatever version.

## v3.14.15, 2026-09-07
### Added
- DealerKit wins on arrival (Steven). On every automatic DealerKit run,
  a car DealerKit holds In Stock while BidBrain still has it Due in is
  checked in here with the arrival date DealerKit holds, its DealerKit
  check in stamped, and a timeline line "Checked in, matched from
  DealerKit". A sold car is left alone. The run reads the stage of every
  not yet arrived car that is on DealerKit, in the same session.
- The DealerKit check (dk_check) now carries the car's stage in words
  (`stage`: Due In, In Stock, Sold), the raw `life_cycle_status` and
  `delivery_on`, for Dealer OS to show on the row.
### Changed
- Check in takes DealerKit's date. When Check in (the button or the
  Status dropdown) finds DealerKit already has the car In Stock, the
  arrival time on BidBrain is rewritten to DealerKit's delivery date,
  with a timeline line saying so, instead of the moment the button was
  pressed. An arrival time set this way is a day, not a moment.

## v3.14.14, 2026-09-07
### Fixed
- Status follows the check in, the older cars: a car from before the rule
  can read In stock with no arrival time on record. v3.14.13 treated such
  a car as not checked in, so picking Due in on it was a plain write with
  no question asked (found live on LM67BPE). It now counts as arrived:
  Due in on it is the undo, with the same DealerKit rule, and In stock on
  it changes nothing.

## v3.14.13, 2026-09-06
### Changed
- Status follows the check in (Steven). Picking a finished Status answer
  (In stock) on a car not yet checked in now IS the check in: the arrival
  time is stamped and, if the car is on DealerKit, it is checked in there
  too, the same as the Check in button. Picking any other answer on a
  checked in car takes the check in back and clears the arrival time,
  allowed only while DealerKit has not been checked in; once DealerKit
  shows the car In Stock the change is refused with a note to change it
  on DealerKit. One door for the Mac's own page and for Dealer OS
  (`serve.apply_purchase_field`, `db.set_stock_status`,
  `db.undo_check_in`). Before this a Status change wrote the word alone,
  so a car could read In stock with no arrival time and never reach
  DealerKit.

## v3.14.12, 2026-09-06

### Changed
- The Live Auctions screen now understands the filters Dealer OS saves
  since its v3.8.0: several makes, models, service histories, condition
  grades or body styles at once (a comma list in the same key, meaning any
  of those), and year, engine size and owners as ranges (`yearmin`,
  `yearmax`, `enginelitresmin`, `enginelitresmax`, `ownersmin`,
  `ownersmax`). A view with one of those used to show nothing here, or
  filter less than it does in Dealer OS. The screen's own boxes are not
  changed: a value they cannot show is carried through the view, the box
  reads Any, and the list, the count and the view chip honour it. Touching
  that box by hand drops the carried value, so what you see is what
  filters. A blank engine size or owner count passes a range, as it
  already passed an exact match. Contract still schema 1, additive only.

## v3.14.11, 2026-09-06

### Fixed
- Deleting a hand added car now takes its own cancellation record with it.
  A hand added car that was cancelled and then deleted left its plate in
  the walked away list for good, kept off every future shortlist and shown
  as a grey row in the Cancelled view with nothing behind it (found on the
  two test cars, TE57DOS and LO68GXE). Only a hand added car's own
  cancellation writes that record, so only that record goes; a platform's
  own cancelled list is never touched, and a plate another purchase still
  carries keeps its record. The timeline row says when it happened.

## v3.14.10, 2026-09-06

### Fixed
- A car sent to DealerKit from Dealer OS that DealerKit would not add now
  says why. Found live on the test car TE57DOS: two sends died in
  DealerKit's own Add Vehicle window with "got no confirmation, the page
  may have changed", nothing was written to the car's timeline, and
  Dealer OS's DealerKit tab simply went back to Ready as if nothing had
  been pressed. Now the send waits up to 15 seconds for DealerKit's
  answer instead of 3, looks the plate up before calling it a failure (a
  slow create is a record all the same), and when DealerKit really did
  not add the car the failure carries DealerKit's own words, from the
  window and from its server. Every failure of a send, not only an
  expired login, is written to the car's timeline by BidBrain
  ("DealerKit send failed: ...") so Dealer OS shows it.

## v3.14.9, 2026-09-06

### Added
- Photos on a car added by hand, from Dealer OS. A local trader buy has no
  listing to read photos from, and DealerKit will not take a car without
  them, so until now such a car could never be sent to DealerKit from
  either screen. `purchase.set` now accepts `photo_urls` on a hand added
  car: Dealer OS stores the pictures in its own media store and sends the
  lasting links as the whole list (at most 40, an empty list clears it).
  The Mac keeps the list as the car's gallery, makes the first the
  thumbnail, and writes one timeline row saying how many. The DealerKit
  send already works from links, so the car can go across. A platform car
  is refused, its gallery is the listing's (Steven's decision, 2026-09-06).

## v3.14.8, 2026-09-05

### Changed
- The heartbeat to Dealer OS goes every 5 seconds whatever the Mac is
  doing, instead of every 15 while idle. A run asked for in Dealer OS is
  picked up within 5 seconds rather than 15, so its progress bar appears
  sooner (Steven: "as close to instant as physically possible").

## v3.14.7, 2026-09-05

### Fixed
- The Software update card read "API rate limit already exceeded" the
  first time it was pressed for real. The latest release was looked up
  through gh release list, which goes through GitHub's GraphQL allowance
  that the whole account shares. It now goes through the REST route
  (gh api releases/latest), which has its own, far larger allowance. A
  failed lookup is remembered for two minutes instead of thirty, and the
  last release that was seen is kept rather than forgotten.

## v3.14.6, 2026-09-05

### Added
- Diagnostics for Dealer OS. The Mac now sends Dealer OS a diagnostics
  document (`POST /api/bidbrain/diagnostics`): the last 300 lines of every
  log under data/logs, the full run records and site health (the heartbeat
  only carries each summary's first line), the live progress file and the
  server process. It goes when Dealer OS asks (a new `diagnostics.pull`
  action, answered in `last_command` with key "diagnostics"), on the tick
  after a run finishes, and otherwise at most every 10 minutes and only
  when a log has grown. Steven's decision: the Mac Studio stays behind the
  office router, never opened up, so anything Dealer OS needs to see, the
  Mac sends.

## v3.14.5, 2026-09-05

### Added
- Update from Dealer OS. The heartbeat now carries a `version` block (the
  release this Mac is on, the newest GitHub Release, whether they differ,
  kept 30 minutes between lookups), and a new `update.start` action runs
  the same one click update the Mac's own Settings page has: fetch the
  tags, check out the release, restart the server. Refusals (a run in
  flight, no newer release, local changes) come back in the heartbeat's
  `last_command` with the reason. Steven's decision: the Mac's own screen
  is never needed for anything, so moving to a new release is a button on
  Dealer OS's BidBrain Settings page.
- A Release workflow: pushing a `v*` tag cuts the matching GitHub Release
  with that version's CHANGELOG section as the notes, so a release no
  longer needs the gh tool on the Mac.

## v3.14.4, 2026-09-05

### Changed
- Retail follows an edit. Dealer OS now sends the retail price to DealerKit
  the moment a retail estimate is edited on a car that is on DealerKit
  (Steven's decision). That job always overwrites DealerKit's own figure
  (already so since v3.13.3, confirmed and now written into the contract);
  only a send whole leaves an existing DealerKit retail price alone. The
  automatic 16:15 run now also sends the retail price on any car on
  DealerKit whose last DealerKit check shows a different figure, so an edit
  that landed while the Mac was busy still goes across. Working list cars
  only: a finished or checked in car's DealerKit retail price is the
  team's own and the run never rewrites it (68 old records would have
  been overwritten with unrounded estimates on the first run otherwise).

## v3.14.3, 2026-09-05

### Changed
- The automatic 16:15 DealerKit run no longer sends a car to DealerKit
  whole by itself. A car goes to DealerKit only when a person presses Send
  in Dealer OS (Steven's decision). The run still picks up chips,
  transport fees, check ins and removals on cars DealerKit already has,
  exactly as before. The contract now also says which field the Send
  preflight reads for "no collection date yet": `collection_date`, the due
  in date, never `collection_arranged`.

## v3.14.2, 2026-09-05

### Changed
- The mileage now goes in beside the registration on DealerKit's own "Add
  Vehicle To Stock" form when a car is created (Steven: "the mileage
  should be entered on the add vehicle to stock modal alongside the reg at
  this very point"), so the record starts with the real figure and
  DealerKit never writes a guessed one. The mileage log entry is kept only
  for a car DealerKit already holds with a different figure.
- The due in date is no longer sent to DealerKit, by a send or by the
  automatic run (Steven: "id rather have the checkin feature work when we
  decide to check it in rather than dealerkit automate it itself"). A date
  on the record made DealerKit hide its own CHECK-IN button and move the
  car to In Stock by itself when the date arrived. The Dealer OS `due_in`
  button is refused with a plain sentence. A check in now goes through
  DealerKit's own CHECK-IN form with the arrival day; a record that still
  carries a date from before is moved by writing the arrival day onto it.

### Fixed
- The check in kept failing with "the CHECK-IN button was not found" on
  any DealerKit record carrying a photo, which looked like a broken page
  and for a while like a broken record (FX68KAE included). It was neither.
  DealerKit draws the lower half of a vehicle's overview, the Due In
  banner with its CHECK-IN button, Vehicle Details and the rest, only once
  that half scrolls into view, and a record with a photo pushes it below
  the fold. A person scrolls without thinking; the automation never did.
  It now scrolls the page like a person before looking for the button,
  and clicks the button through the browser so a click can never land off
  screen. No record needed repairing.

## v3.14.1, 2026-09-05

### Fixed
- Three settings sync checks in the test suite ran against the Mac's REAL
  settings table instead of a temporary one: green on this Mac, red on a
  fresh CI database (CI had been red since v3.13.3, found when v3.14 was
  pushed), and one of them WROTE `details_style` into the live table with a
  year 2099 stamp, which would have made every later Dealer OS change to
  that setting count as older for ever. The checks now use their own
  temporary database, and the real row was re stamped as saved today (the
  value, popup, is Steven's own). The suite is hashed against the real
  database before and after a run; it leaves it untouched.

## v3.14, 2026-09-05

DealerKit from Dealer OS, the redesigned tab: a car is sent whole once its
collection date is confirmed, then kept in step. Everything is additive
(the push's `schema_version` stays 1), an older Dealer OS keeps working.

### Added
- `purchase.dk_send {id}`: one job that creates the DealerKit record if
  there is none and sends the registration, mileage, purchase price,
  buyer's fee, photos, service history and V5, retail price and due in
  date in one go. Refused with one plain sentence per missing thing (no
  mileage, no purchase price, no fee, no photos, no retail estimate, no
  collection date, a cancelled sale, the Mac busy), all together in one
  message so Dealer OS can show them.
- The mileage now reaches DealerKit. It never did before: DealerKit wrote a
  guessed figure of its own onto every new record. A send writes BidBrain's
  real mileage as a mileage log entry, so DealerKit's `vehicle.mileage` is
  the Mac's figure. The buyer's fee goes across too, as the Buyers Premium
  line on the supplier's expense.
- Four follow ups for a car already on DealerKit, each as a `purchase.dk`
  action and each picked up by the automatic run: `chip` (the new price and
  fee after a chip), `transport` (the transport fee as the Delivery Charge
  line), `checkin` (Due In to In Stock with the real arrival date; a
  `purchase.checkin` now does this by itself when the car is on DealerKit)
  and `remove` (a cancelled sale's record deleted while DealerKit still
  shows it Due In; left in place with a sentence once In Stock). None of
  them ever creates a record.
- `purchase.delete {id}`: a hand added car leaves the Purchases list for
  good, its timeline kept with one last row of kind `deleted`. Refused while
  the car is on DealerKit (cancel those instead) and for any platform buy.
- Six new purchase stamps in the push: `dk_mileage_pushed_at`,
  `dk_fee_pushed_at`, `dk_chip_pushed_at`, `dk_transport_pushed_at`,
  `dk_checked_in_at`, `dk_removed_at`.
- The automatic DealerKit run now has a real daily time,
  `com.bidbrain.dealerkit-purchases` at 16:15 (after the 15:30 DealerKit
  login and the 15:50 purchases sync), so it appears in `run_schedule`. It
  sends whole any car whose facts are all in place and picks up chips,
  transport fees, check ins and removals not yet sent, the same rules as
  the buttons.

### Changed
- Recording a chip, or changing the transport fee, clears the matching
  DealerKit stamp so the next automatic run sends it.
- Deleting a hand added car keeps its timeline (it used to wipe it).
- A repeat send no longer doubles the photos: a record that already holds
  as many photos as BidBrain has is left alone.

### Fixed
- The DealerKit expense category group is named differently per account
  ("Stock Purchase" on Right Drive's, "Cost of Purchase" on Really Easy
  Car Credit's), found live; both are tried.
- A removal is keyed by registration, so a cancelled sale sharing its
  registration with a live one (the Ford Puma, sold on both platforms the
  same day) is never removed; refused with a sentence, in the door and in
  the automatic run.

## v3.13.6, 2026-09-05

### Added
- A DealerKit contact can now be deleted (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. Used to
  clean up the 13 test contacts this session's own testing had left behind
  in the real account.

## v3.13.5, 2026-09-05

### Added
- The DealerKit Price button now creates the supplier as a real DealerKit
  contact automatically when it does not already exist, then carries on and
  types the price as normal. Before this, a genuinely new supplier had to be
  created in DealerKit by hand first, or the price silently would not save.
  Proven live, 6 fresh attempts in a row, each with a supplier name never
  seen before.

### Fixed
- The new contact step occasionally needed a second try (found live, about
  1 in 6 real attempts, DealerKit's own known variable timing), so it now
  retries once before giving up, the same discipline already used
  throughout this file.

## v3.13.4, 2026-09-05

### Fixed
- A purchase edit made on the Mac's own page (a field, a chip, a cancel, a
  comment, an override, add or delete by hand, the page settings) now
  reaches Dealer OS the same way a star, hide or view already did, instead
  of sitting unsent until the next daily run. Found live: a registration
  correction sat unsent for 12 minutes.
- The DealerKit Price button, for a genuinely new car, now selects the
  purchase's own supplier before typing the price, since DealerKit silently
  refuses to save Purchase Price without one, found live by hand testing.
  Two further bugs on the way there are also fixed: the field finder had
  started typing into a new "Purchase Invoice Ref #" field DealerKit added
  since this was last built, and, once fixed, was matching a huge invisible
  ancestor of the whole page instead of the real, short label.

## v3.13.3, 2026-09-04

### Changed
- Ticking Retail price on its own now overwrites the figure on DealerKit, so
  changing a retail estimate in Dealer OS and sending it makes the two match.
  The all in one push still leaves an existing DealerKit retail price alone.

### Fixed
- A DealerKit push now says what each item actually did, in the log and on
  the car's timeline: set, changed from one figure to another, already right,
  or nothing to send. Before this, a price that merely already matched read
  as "Sent to DealerKit: price", and a skipped retail price gave a reason
  that was not true.

## v3.13.2, 2026-09-04

### Fixed
- The shortlist sync to Motorway and Carwow judged its own click too soon:
  both sites keep showing the old star for a few seconds, so a click that
  genuinely worked was reported as a failure, left the car pending and
  turned the run light red. Both now wait for the state to change and
  confirm with a reload, retrying the read.

### Added
- DEALER_OS_HANDOVER.md: what Dealer OS can send, which version each was
  proven on, and what a future session should know.

## v3.13.1, 2026-09-04

### Fixed
- A setting applied from Dealer OS is now stamped with the action's own
  time, not the Mac's clock, so two saves made seconds apart never make
  the second look older than the first and get skipped. Found live during
  the step three proof: a platform gate save right after a spread save was
  skipped as "the Mac saved flat_spread more recently".

## v3.13, 2026-09-03

### Added
- The way back for settings, step three: Dealer OS can send back the
  purchases dropdown answers, the column layout, the journey flow, the
  buying rules a dealer changes day to day, and the integration switches
  (settings.set {section, value}), each applied through the same checks
  the Mac's own Settings page makes, newest change wins per key.
- The push carries the Mac's whole settings page (`mac_settings`): the
  buying rules, integrations, hidden connections, the fee band tables,
  the engine bans and the run schedule (the last three read only), so
  Dealer OS can draw a settings page from the push alone.

## v3.12.1, 2026-09-03

### Added
- Step two of the purchases way back proven end to end on the live site:
  a car added by hand, a chip, a check in, a cancellation, a DealerKit
  check and a saved view (and its delete) made on Dealer OS all reached
  the Mac, applied through its own doors with the person's email, and
  came back.
- Every document scan and purchase photo now has a lasting link in
  Dealer OS (917 files sent, 3 shrunk to fit, none failed).
- A DealerKit job started from Dealer OS logs the Mac back into DealerKit
  once with the stored password if the saved login has expired, and
  writes its own outcome, success or failure, on the car's timeline.

## v3.12, 2026-09-03

### Added
- The way back for purchases, step two: a chip, a cancellation, a check
  in, a car added by hand, the DealerKit buttons and saved views made on
  Dealer OS are pulled and applied on the Mac through the same doors its
  own Purchases page uses, with the same checks and refusals, each logged
  with its reason and the person's email on the timeline.
- Saved views from Dealer OS use the Mac page's own filter keys, so a view
  saved there applies on the Mac's own screen too.

## v3.11.3, 2026-09-03

### Added
- The way back for purchases, step one, proven live: a Video change made
  on Dealer OS reached the Mac in seconds, landed on the right car with
  the person's email on its timeline, and was pushed back automatically.
- The document upload now covers a purchase's photo files too, downsizes
  any image over 4 MB to a 2000 px JPEG first, and stops after three
  failures in a row so a broken store can never hold up the push.
- The spec for the way back, step two (chips, cancellations, check in,
  cars added by hand, the DealerKit buttons, saved views) is in
  DEALER_OS_PUSH.md, not built yet on either side.

### Fixed
- A missing or refused document route reads as "not open yet", never as a
  bad token.

## v3.11.2, 2026-09-02

### Added
- The push to Dealer OS also carries every purchase's timeline
  (`purchase_events`) and the Didn't win page's saved views.
- Document scans (service history, V5) get a lasting link: each file is
  uploaded once to a Dealer OS route the contract now describes, and the
  push carries the link instead of a path on the Mac. Until Dealer OS has
  the route, nothing is sent and nothing fails.
- The way back for purchases, step one: notes, the dropdowns, paid for,
  collection arranged, the retail estimate and due in date overrides, and
  comments made in Dealer OS are pulled and applied on the Mac through the
  same checks its own page uses.

### Fixed
- The test file's summary line sat mid file, so every check added after it
  never ran. Moved to the end; the true count is 1,135.

## v3.11.1, 2026-09-02

### Added
- The push to Dealer OS now also carries the Purchases working list, the
  Under Offer snapshot and the Didn't win rows (plus the purchase page's
  own settings), the same rows the Mac's own pages draw, minus local file
  paths, so Dealer OS can build those three pages. Read only on the Dealer
  OS side for now; edits still happen on the Mac's own Purchases page.

## v3.11, 2026-09-02

BidBrain inside Dealer OS. The Dealer OS BidBrain page is now the cockpit,
rebuilt pixel for pixel, with a two way link to the Mac. The Mac's own
cockpit is untouched and stays the safety net until both owners have
confirmed the switch.

### Added
- The push to Dealer OS now carries everything the cockpit draws: every
  card field, the traffic light insight rows, the full record drawer
  (facts, pricing receipt, flags, CompMatch comps, history), photos, Top
  picks rank, taste match, stars, plus page level settings, notices, model
  families, filter scope, platforms, integrations and the run checks. The
  on demand Dealer OS button sends the same annotated, hide filtered
  document a run does. Over 3.5 MB the photo galleries are trimmed and the
  push retried once.
- The way back (`bidbrain/dealer_os_sync.py`): every star, hide, unhide,
  saved view, display setting and run request made in Dealer OS is logged
  there; the Mac's serve.py pulls that log after a heartbeat every 15 s
  (5 s while busy), applies each through the same functions the cockpit's
  own buttons use, rebuilds cockpit.html and echoes its state back in the
  next push. Newest change wins per key. `python3 daily_run.py
  --dealer-os-pull` runs one pull by hand; every run pulls first.
- The heartbeat carries the run light, progress, run board, connection
  health, the run registry and which request the Mac last answered, so
  Dealer OS shows the same light, bar and Run modal as the cockpit and a
  run asked for there starts on the Mac.
- Dealer OS's Display choices (how Details opens, the price ceiling) follow
  the Mac's and the Mac's follow Dealer OS's, newest stamp wins.
- BidBrain's warnings in each push become Dealer OS bell notifications.

### Changed
- `bids`, `hidden_cars` and `saved_views` carry `updated_at`; saved views
  carry a stable id and a delete marker so both screens agree on them.

### Fixed
- `serve._dealer_os_snapshot` referenced `daily_run` without importing it,
  so the live sync loop would have failed on every tick.

## v3.10, 2026-09-02

### Added
- **Top picks, the cockpit's new default view.** Every priced car gets a
  score from signals the run already has (room between reserve and max
  bid, a gap in stock, your taste, what CompMatch knows, whether the car
  is new since yesterday, a mark down for no service history), and the
  best 25 open first, in rank order, with "Show all N" one tap away and
  an "All cars" row in the views menu. Cards carry a "#N pick" badge and a
  "New today" badge. A "New since yesterday" sort joined the sort list.
  Nothing about pricing or status changes, this only decides what sits at
  the front.
- **A car record drawer on the cockpit.** Details on a card now opens the
  same slide in panel Purchases has: photos, the car's facts, a receipt
  showing exactly how the max bid was worked out, and three tabs,
  CompMatch (the suggestion and the real comparable sales it used),
  History (every earlier sighting and reserve, your own sold record for
  the model, stock) and Flags. Star, hide and the link to the auction
  house sit in its footer. **Settings > Look chooses which one Details
  opens**, the drawer or the small pop-up it replaced, per dealership.
- **Top picks read as one list under All**, #1 to #25 whatever the auction
  house, instead of being split into per platform sections with Motorway
  first (which buried a Carwow #1 a long scroll down).
- **Top picks now lean on your own sold history.** A model you have sold
  before is the strongest signal (more so the more you have sold), the real
  average margin after prep is next, and a quick seller earns a little extra.
- **New ban: Ford 1.1 petrol, 2014 to 2019**, treated exactly like the Ford
  1.0 EcoBoost (same year window).
- **Every run now starts, reports and finishes the same way, on every page.**
  One registry of runs (`bidbrain/runs.py`) replaces five hand written maps in
  the server; one shared finish means a run can no longer stop without saying
  so (a switched off run reads as "skipped", grey, never red); one shared
  display kit gives all five pages the same progress bar, the same toast
  (green done, red failed with the reason) and the same waiting loop. Didn't
  win, Under offer and Settings gain the bar they never had; Purchases loses
  its browser alert boxes; the four Maintenance runs now behave like the
  auction house runs; a red light click opens the run board on every page.
  A one car push or check can no longer be mistaken for the bulk run.
- **The Run modal is on every page.** The same board (auction houses, last
  run, Maintenance cards, logins) opens from the run button on Live
  Auctions, Purchases, Didn't win, Under offer and Settings alike. The
  Purchases sync, Didn't win sync, Didn't win full history and DealerKit
  records checks are now rows on it, so the two separate sync pickers are
  gone. The board is two tabs, Auction houses and Maintenance, each a table
  of rows with the same anatomy (tick, name, state, result, age); Live
  Auctions opens on the auction houses, every other page on Maintenance.
  One Run does the ticked auction houses first, then each ticked check in
  turn, and the footer says exactly what will happen. Toasts sit just under
  the top bar, on the eye line, and the board opens the instant you click.
- **DealerKit logs itself in.** The three DealerKit checks (stock, purchases
  push, records check) try the saved session, then the saved password, and
  only wait for a person at the Mac if both fail, so they can be started
  from any device. The LE Capital funding sync does the same for both its
  logins.
- **One more car per row on Live Auctions** at every screen size: 4 across on
  a 13 to 14 inch laptop (was 3), 6 on a 1920 monitor, 11 on a 34 inch.
- **All cars is the default view again**; Top picks is one tap away in the
  views menu (whose count now reflects the real 25) or the link beside it.
- **Purchases views remember the column layout**, Airtable style: resizing,
  reordering or hiding a column saves straight to the view you are on (or to
  the default layout on All cars) and comes back with it.
- **The watchlist sync stops chasing lost causes.** A car that has left the
  list is dropped from the sync queue, and a car that fails to bookmark
  three times is left alone (its error stays on file) until it is starred
  again.
- **Fixes found by the audit:** stars no longer queue for a second Motorway
  account that has never logged in on this Mac (the "0 of N synced" red
  light); the DealerKit auto login moves to 15:30 so it runs before the
  15:50 purchases sync; the purchases sync records health per platform.

### Changed
- **The plumbing is hidden.** The Run menu folds its nine maintenance
  checks into one collapsed Maintenance row that opens on demand, or on
  its own when a check is failing. Settings went from eight tabs to four:
  Buying rules (with the auction house picker inside it), Purchases,
  Connections, Look. Every old link still lands where it did.
## v3.9.1, 2026-09-02

### Fixed
- Right Drive's own `DEALER_NAME` (used both by CompMatch's comp-sharing
  export and as a sanity-check label on the Dealer OS push) was still
  "Right Drive" while Dealer OS has "RightDrive Car Finance" registered
  against its API token, so every push surfaced a visible mismatch
  warning on screen. Updated to match, which moved this dealership's own
  shared CompMatch file from `shared_comps/right_drive.json` to
  `shared_comps/rightdrive_car_finance.json` (a real rename, refreshed
  with current data, not a stale copy under a new name).

## v3.9, 2026-09-02

### Changed
- **Ford 2.0 diesel is now a banned engine** (Steven 2026-09-02), TDCi and
  EcoBlue alike, any year, caught from the fuel field as well as the engine
  text. The only buying rule change in this batch.

### Fixed
- **Cazana logs itself back in mid run.** When Cazana fails four lookups
  in a row the run now runs the same keychain login the 17:00 job uses
  and retries once, instead of holding every remaining car. Three runs
  in the log had died this way; on a one day Motorway auction those cars
  were lost for good.
- **Kia Ceed valuations were always refused.** Motorway spells it "c'eed",
  Cazana "Cee'd", and the match required the exact token. Punctuation is
  now ignored on both sides. Kias were 28 of 115 held cars in a fortnight.
- **The assessments table was saved 3 to 7 times per day** (once per
  platform as each finished, the whole list each time), inflating every
  count taken from it. One row per car per day now, and the history was
  de-duplicated (47,879 rows to 20,309, backup kept).
- **Auction4Cars cars it never visited were rejected as "engine unknown"**
  instead of their real reason; the engine text is now set at read time.
- **"Owners missing" and "reserve missing" no longer clutter a rejection
  that already has a real reason** (about 200 car-days a week, all cars
  that had failed the cheap first check before their page was ever read;
  a live check showed the facts were on the page all along).

### Added
- **The first step of getting BidBrain inside Dealer OS.** After every
  run, BidBrain now sends its finished list (the shortlisted and held
  cars, each with its price, reasons and CompMatch suggestion) into
  Dealer OS as one JSON document, and the Run modal gained a "Dealer OS"
  button to re send the last run on demand. Dealer OS only displays the
  list; every scraper and the whole buying brain stay on the Mac, so
  there is one set of rules that cannot drift. The receiving route on
  the Dealer OS side is still to be built, against `DEALER_OS_PUSH.md`.
  Configured per dealership in `dealer_config.py` (`DEALER_OS_BASE_URL`,
  `DEALER_OS_API_KEY`, a shared secret that must match `BIDBRAIN_API_KEY`
  in Dealer OS's own `.env.local`); blank means "not set up", never a
  failure. Its own on/off switch and a "Dealer OS" row (no login, it has
  none) on Settings > Integrations, its own line on the sync light.

## v3.8, 2026-09-01

A traffic light in every page's nav bar so a sync is visible wherever you
are, a full sweep that caught and fixed around 30 real inconsistencies
across the app in the process, and the Retail est. formula becoming
genuinely per-dealership instead of one fixed number for everyone.

### Added
- **A sync light in the nav bar of every page.** Lives inside the
  existing Run/Sync button on each page, colouring it green and spinning
  while something is running, amber if it goes quiet for 90 seconds, and
  red if the last run of something failed — visible from any page, not
  only the one that started it. Clicking it opens the Run modal, rebuilt
  as a real status board: when each platform and check last ran, whether
  it worked, and a Log in button right on the failed row.
- **The Retail est. formula is now per dealership.** Two new settings
  (Settings > Overview): the flat margin added on top of Cazana's 115%
  before rounding (was always £500), and the price ending it rounds to —
  either the four specific bands one dealership's own real pricing uses,
  or a plain "every £100, ending 99" for the other. Each dealership keeps
  its own figures; nothing changes unless you touch these settings.

### Fixed
- The purchases sync (the Sync now button, and the daily 3:45pm job) now
  respects platforms switched off or hidden in Settings. It previously
  always tried to read every platform regardless, and a hidden platform's
  read failure painted the sync light red app-wide for a connection that
  was deliberately switched off.
- The Run modal no longer runs off the top or bottom of the screen on a
  smaller laptop display with no way to reach the rest of it — it now
  caps to the visible height and scrolls internally, with the header and
  the Cancel/Run buttons always staying in view.
- Every completed daily run used to leave the sync light showing a false
  "failed, stopped without finishing" for hours afterwards, even though
  the run had genuinely worked — the single worst thing the sync light
  itself did wrong, caught and fixed the same day it shipped.
- A full sweep of the sync light and the last few commits caught and
  fixed roughly 30 further real inconsistencies: starting a sync used to
  briefly show the previous sync's leftover message as if it were a fresh
  problem; stopping a run showed a false green "finished" tick; three of
  the Run modal's own buttons broke their own display on first click;
  "Done" could show right next to a red failure light; a sync that failed
  completely could still record as a success; and several purchases-page
  filter dropdowns silently did nothing when changed, a pre-existing bug
  surfaced while checking this same area.
- Two DealerKit functions carried one dealership's own internal team name
  hardcoded as a silent default. Neither is currently used by anything in
  the app, so this was never live, but both now require the team to be
  stated explicitly rather than guessed, so it can't misfire quietly if
  either is ever wired up later.

## v3.7, 2026-08-31

The manual purchase path rebuilt properly, and the left side panel removed
from the whole app.

### Added
- Adding a car by hand is now correctable, not a one-shot form: on a hand
  added row the price, fees, purchase date and supplier can all be edited
  afterwards from the drawer, each edit logged to the timeline with the
  before and after. A platform row still refuses every one of these, the
  platform stays the source of truth for its own cars.
- The same reg can no longer be added twice — refused, naming the existing
  row — unless the earlier one was cancelled, so a genuine re-buy still
  works.
- A real Supplier field: a managed dropdown, editable in Settings, with
  inline creation from both the Add car screen and the row's own dropdown
  menu (pick "+ Add a new supplier...", type the name, tick to save). A
  name already on the list is picked quietly rather than refused. Person
  collecting gained the same inline-add control.
- VAT on a hand added car's fees is now a tick per fee, computed
  automatically at 20% (off by default), with a live running total as you
  type. Correctable afterwards from the drawer via a small `+VAT` tag on
  each fee line. A platform row's own invoiced VAT is never touched.
- A Retail estimate field on the Add car screen, landing in the same
  override slot the row's own pencil already edits, so it survives the
  settings retail cap untouched.
- Person collecting and Collection arranged can now be set when adding a
  car by hand, both optional.
- **Delete this car**, for a hand added row only — removes it and its
  whole timeline for good, with a confirm step. A real platform purchase
  still only ever leaves the list through Cancel.
- The Add car modal is two columns now, with required fields marked,
  real labels instead of placeholder text for the fee boxes, and a
  section grouping (car / purchase / collection) instead of one long
  stack.

### Changed
- A hand added car no longer shows a Video stage, a Video column value,
  or an Auc status tracker — none of it applies to a car nobody sent a
  platform crew to film. The journey for a manual car is now Bought →
  Collection → Arrived. The Bought stage reads "Bought from [supplier]"
  instead of "Won on Manual".
- A hand added car is excluded from CompMatch's own comp pool: a
  negotiated trade price is not a competitive auction clearing price, and
  counting it was biasing every suggestion downward.
- The left side panel (Dashboard / Stock / BidBrain / Customers / Reports,
  mostly disabled "Soon" placeholders) is gone from every page. The
  dealership's own logo, the one real thing it held, moved to the top nav
  bar instead. The content panel is meaningfully wider as a result — the
  live auction grid now fits 3 cards per row at a normal laptop width
  instead of 2.
- The Add car modal's spacing rebuilt: labels and fields no longer fight
  each other for vertical space (a label's own top margin and the grid's
  row gap were both adding space at once), a real divider now separates
  the form from its Cancel/Add buttons, and a lone Mileage field no longer
  sits half-orphaned next to dead space.

### Fixed
- The "+ Add a new supplier" flow originally shipped with no visible way
  to confirm a typed name, and the dropdown right above it kept reading
  "Not recorded" the whole time, looking broken. Rebuilt as one control
  that replaces itself (dropdown → name box → back to dropdown, now
  selected) with an explicit tick to confirm, never two controls
  disagreeing on screen at once.

## v3.6, 2026-08-30

Five small nav and head bar tidy ups, all markup and CSS, nothing touching
the gate, pricing, CompMatch, readers or DealerKit.

### Changed
- The count beside the auction tabs is gone from Live Auctions, Purchases
  and Didn't win: the views chip right next to it already shows the same
  live number (confirmed live, switching a platform tab moves both
  together). Under Offer keeps its own count, it has no views control to
  fall back on.
- The More button restyled to match the views chip exactly — same height,
  padding, border, weight, hover, and a real chevron on the right, reusing
  the existing chevron icon rather than a second copy.
- The nav search box widened, its placeholder was genuinely truncating
  (144px of usable space against roughly 157-164px of text), and given
  `font:inherit` so it renders in the app's own Plus Jakarta Sans instead
  of the browser default Arial it had always silently fallen back to.
- The "Calendar, coming soon" placeholder removed everywhere. It was
  never wired to anything.

### Fixed
- The shortlist star's red badge no longer shows a bare 0. The cockpit
  was the one page of five missing the `:empty` CSS rule the other four
  already had, and `applyView` was separately writing the literal `"0"`
  back into it after every load regardless — both had to go for the fix
  to hold. Verified live both directions: starring a car shows the real
  count, unstarring clears the badge entirely.
- The widened search box still clipped "model" to "mode" in real use even
  though the box-model numbers said it fit. Root cause: Safari reserves
  internal space for `type="search"`'s own native icon on WebKit,
  invisible to any CSS measurement — added `-webkit-appearance:none`
  (this box already draws its own magnifying glass, so nothing native was
  wanted there) plus a little more width as headroom.
- The chevron restyle nearly shipped invisible: the views chip's own
  chevron rule is scoped to `.viewchip .car`, and the test suite's
  orphan-class scan would not have caught reusing it as-is, since it
  harvests `car` out of that descendant selector and counts it as already
  styled. Generalised the rule to cover both buttons instead of adding a
  second one.

## v3.5, 2026-08-30

The live auction card's own bottom section redesigned: Details, View on
[platform] and "···" are one action row, and Details now opens a small
floating panel that overlays the gauge and chips instead of pushing the
card taller. Explored first as a series of published design artifacts
(ten alternative approaches considered, one built out and iterated with
real feedback) before landing in the real cockpit.

### Added
- Details panel: click the pill to see every flag this car carries, in
  full sentences, plus its Glass's/Cazana valuation chips and the Cazana
  deep-link button, all in one place. Opens above its own trigger,
  overlaying the card's own gauge and chip row, closes on an outside
  click, Escape, or its own close button. Only one panel is ever open
  across the whole page at once.

### Changed
- The old row of small traffic-light flag icons, its click-to-expand
  block, and the separate Valuations toggle are gone, folded into the
  one Details panel. A car with nothing to flag and no valuation shows
  an honest "Nothing more to add on this car" line rather than an empty
  box.
- Comfortable/Compact removed from the toolbar: it only ever existed to
  bulk expand or collapse those two blocks, and there is nothing left
  for it to control once nothing expands in place any more.
- Because nothing below the gauge can grow the card's own height any
  more, the row-alignment dead-space bug reported against the previous
  card design (a short car's own card stretched to match a taller
  neighbour, leaving a blank void where its flags used to end) cannot
  recur, by construction rather than by patching around it.

### Fixed
- `.hf-card` also carries the shared `.card` class, which declares its
  own `overflow:hidden`; simply removing `.hf-card`'s own declaration
  left that value in effect from the other rule and the new panel was
  still being clipped. Needed an explicit `overflow:visible` on
  `.hf-card` to actually win the cascade, caught live before shipping.

## v3.4, 2026-08-30

A shared "Frame" (filter chips, a More filters drawer, a live result count,
and one views control) built once and rolled out to all five pages,
replacing several separate implementations that had drifted apart from each
other over time; a defensive hardening pass across the whole app so a
missing element or a bad platform read fails loud instead of silently
breaking a page; and a long tail of real bugs found by a full UX review, from
cards misaligning row to row to cancelled purchases only showing 88 of 541
real cancellations. No gate or pricing logic touched anywhere in this
release.

### Added
- The shared Frame: one filter chip row, a "More filters" drawer, and a live
  result count, common to all five pages. On the cockpit and purchases it
  also carries one views control, merging what used to be a fixed "slice"
  chip and a separate saved-views button into a single dropdown that reads
  "All cars", the name of whichever saved view you exactly match, or
  "Custom" the moment you deviate. Rolled out page by page: Under Offer
  first, then Lost (which gained real slices and range filters, losing its
  old Stats block), then Purchases (which also gained a real Margin column
  and promoted Add car into the toolbar, dropping its own Stats block, and
  gained a Stop control), then the cockpit itself.
- The cockpit's own filter engine rebuilt to be criteria driven rather than
  reading the DOM directly (Run A), so gap fillers, Auto only and Held back
  could become ordinary filters instead of hard-coded chips (Run B), and
  every remaining range filter (mileage, reserve, distance and so on)
  became a real dual-handle slider, built in house rather than pulled from
  a library, then rolled out across the whole app (Run C/D).
- Every filterable data point the cockpit and the purchases working list
  actually hold is now a real filter, each scoped to only the platforms
  that actually publish that field.
- A five-pass visual design cleanup across the Frame: shared controls that
  were rendering as raw, unstyled browser defaults, one consistent scale
  for the frame's own panels and buttons, one labelled-field shape and
  rhythm through the purchases drawer, trimmed repeated filter copy with a
  scope note that belongs to its own field, and saved-view starring working
  on purchases too.
- The settings gear swapped twice in quick succession: first to a plainer,
  more recognisable gear, then to lucide's own Settings glyph once Mark saw
  it and preferred it.

### Changed
- A hardening pass across all five pages: guarded element access so a
  missing DOM node degrades instead of throwing, a loud rather than silent
  failure when the price ceiling setting can't be read, two separated
  failure domains on the purchases page so one bad write path aborts
  cleanly, rows escaped, UK dates and en-GB pound formatting enforced
  everywhere, sub-860px layout containment on all five pages, and Under
  Offer properly joined the BidBrain nav section.
- The card foot (flags, Valuations, the platform CTA) redesigned so a car
  with few flags no longer misaligns against a row neighbour with many: the
  card is anchored at both ends, only the flags are allowed to float, and
  the AI-suggests line reserves its own space even when it's empty, so the
  flags start level on every card whether or not that specific car has a
  CompMatch suggestion yet.
- Read full history on the Lost page now confirms before it runs and blocks
  the page while it does, and moved into the same sync modal purchases
  already uses rather than its own standalone button.
- Didn't win's old fixed slice chip (Last 30 days, Lost by under £250)
  converted onto the same one views control as everything else, and the
  slice chip mechanism was then retired outright from the codebase, nothing
  renders it any more.

### Fixed
- Cancelled purchases were showing 88 of 541 real cancellations: walked-away
  cars (a cancellation recorded under a different reason) weren't being
  folded in. They're all one thing now, filterable by the real cancellation
  reason, a filter which only ever appears once you've actually filtered to
  Cancelled.
- The cockpit's own filter and view counts were ignoring the search box, so
  a count could disagree with what was actually on screen; a saved
  purchases view was separately dropping almost everything it was meant to
  filter to; Didn't win's two headline counts could legitimately disagree
  with each other.
- The cockpit was effectively unusable on a phone: the toolbar went dead
  under the run progress bar, the Filters button silently switched off your
  shortlist, a stale count badge lingered after a filter changed, and the
  Run button's own red "needs attention" dot was backwards, showing when
  everything was actually fine.
- The "All cars" row on the purchases saved-views list did nothing at all
  when clicked.
- A disabled platform could freeze the whole cockpit's own date.
- The Shortlisted badge was counting stars on lots whose auction had
  already ended.
- Under Offer never got its Calendar placeholder and understated what the
  search box could actually search; a few smaller ones landed in the same
  pass — a keyboard trap on the flag icons, a duplicate search chip, one
  clearer word on the sync button, and the purchases toolbar's own right
  hand cluster no longer spreads across the whole row.
- Checked whether the cockpit needed the same narrow-screen search
  treatment the other three pages have; it doesn't, for a reason specific
  to this page (recorded in the code rather than silently added and left
  unused).

## v3.3, 2026-08-27

The purchases page rebuilt as a genuine working list, not a report: every
car between winning the bid and landing on site, with its own journey,
timeline, and a real DealerKit sync panel, worked entirely from inside the
app instead of an Airtable alongside it. Built by Steven on his own copy,
reconciled with Mark's own parallel work (Under Offer, shortlist sync, the
Auc Status and Motorway won-date fixes) landing in the same release cycle;
see `CLAUDE.md` for how that merge was resolved. No gate or pricing logic
touched anywhere in this release.

### Added
- The car overlay redesigned as two permanent sections: Car details on the
  left (Car, Money, Collection and logistics, Notes and documents, in
  grouped, always-visible rows), and a right hand panel carrying three
  tabs, Journey, Timeline, DealerKit, so checking the timeline or the
  DealerKit sync state never hides the car's own record. The drawer
  always opens wide enough for the split, dragged wider if you like.
- The Journey tab: a Next up card plus a vertical stage rail (Bought,
  Video check, Collection, Arrived), each stage carrying only its single
  closing control, video status, Paid for, Check in. Chips (a price
  knocked off after agreeing it on the video) and full cancellations are
  recorded from here, both logged to the car's own timeline alongside
  every field edit and comment, one continuous history per car.
- The DealerKit tab is a full sync panel, not a summary: every pushable
  item (price, fees, photos, documents, retail price, due in date) shown
  as on DealerKit, missing there, or ticked here ready to push, with an
  honest "not charged on this purchase" state for whatever does not
  apply, and Upload/Check DK record run straight from the panel.
- Column drag to reorder and a resize grip on every header, both held per
  dealership; a Fields menu to show or hide any column per table or per
  record; a Views menu to save the current filters and sort under a name
  and apply them again with one click.
- Four page views, Due in (the working list), In stock (the archive,
  checking a car in moves it here), Cancelled (every fallen through sale
  kept with its reason) and All, each showing only the columns that mean
  something there, plus a real column footer with car count, price,
  chip and paid rollups.
- A car bought outside the platforms (a local trader) can be added by
  hand, its own record never touched by any platform sync afterwards.
- The uploaded service history and V5 photos are now visible on the car's
  own record, not just the table, opening the same shared gallery.

### Fixed
- Long notes no longer cut the editor off mid word: the scroll listener
  added to dismiss floating menus was catching the note box's own
  internal scroll and blurring it out from under the typist.
- Every price shows pence only when they are real (`£7,500`, not
  `£7,500.00`; `£4,820.80` keeps both digits when it should).
- Every date the app renders reads UK format, `DD/MM/YYYY`, not the raw
  ISO or American order that had been slipping through in a few spots.
- An empty service history reads "No service history" instead of a blank
  dash, in the table and on the record.
- Dropdown cells now carry a visible caret so it is obvious they open a
  menu, and clicking anywhere else, or scrolling, reliably closes it,
  which it previously did not always do.
- A note now saves with a visible tick button as well as Enter, so
  finishing an edit does not depend on clicking precisely off the cell.

## v3.2.3, 2026-08-27

### Fixed
- A car won on Motorway within the last hour reads its own date as, e.g.,
  "57 mins ago"; the date parser only ever recognised hour/day/week/month
  units, so a freshly won car's bought date silently stayed blank even
  though the rest of its purchase data had genuinely synced. The
  purchases page's own default date filter never matches a row with no
  bought date, so the car was correctly synced but invisible until this
  fixed the one shared parser behind the purchases, Didn't win and Under
  offer pages alike.

## v3.2.2, 2026-08-27

An Under Offer page (a bid currently leading, not yet a confirmed sale) and
a real purchases page status fix, caught and corrected the same day it
shipped. No gate or pricing logic touched.

### Added
- A new Under Offer page, reachable from the same Live Auctions/Purchases/
  Lost switcher as the other admin pages, showing every car Motorway's or
  Carwow's own real Bids & Offers > Under Offer screen currently lists as
  a leading bid.

### Fixed
- The purchases page's own Auc Status tracker always showed its Bid stage
  as accepted, even for a Motorway purchase genuinely still at "Awaiting
  confirmation" (caught live on a real example). It now reads the real
  status honestly, matching Motorway's exact rule for when a win actually
  counts as accepted.
- A same-day attempt to also surface that same "not yet accepted" state
  on the new Under Offer page was reverted once it became clear it did
  not match what Motorway's own portal actually shows there; that page
  now shows only real Bids & Offers data, nothing from the purchases
  table.

## v3.2.1, 2026-08-27

The one platform action BidBrain has been approved for since 2026-06-09,
finally built: starring or unstarring a car in the cockpit adds or removes
it from that platform's own real shortlist or watchlist, never a bid. No
gate or pricing logic touched.

### Added
- Shortlist/watchlist sync to Motorway, verified live on a real auction
  car, both directions, genuinely persisting server side. Carwow's own
  mechanism is built and correct (confirmed against the platform's own
  real Stimulus controller source) but still needs a live check against
  an active listing once its stock is out of the daily dead window; a
  real mismatch always surfaces as a visible error, never a silent false
  success either way. Happens automatically in the background within a
  few seconds of a star click, no extra step needed, with a manual "Sync
  watchlist to platforms" button in the Run modal to catch up any
  backlog.
- A second Motorway account, accounts@rightdrive.co.uk, receives the
  same shortlist sync independently (its own separate browser profile,
  its own automated daily login at 06:00), for whoever completes an
  actual purchase, which needs that inbox's own SMS code. Shown on the
  Integrations tab as its own connection, can be hidden the same way as
  any other.

### Fixed
- An automated Motorway login that failed for a genuine reason (a wrong
  password, most commonly) reported a blank error message instead of
  the real one, because the check happened after an extra page
  navigation that had already cleared it. Both Motorway login scripts
  now check immediately after submitting, so a real failure always
  shows its own real reason.

## v3.2, 2026-08-27

Auction4Cars and Dealer Auction join the purchases page, a real DealerWay
logo, seven sites now log themselves back in, and a round of navigation
tidying: a Live Auctions/Purchases/Lost page switcher, a sticky filter row,
a search icon, and Purchases/Didn't Win pulled back out of the left
sidebar once that switcher made them a duplicate path. No gate or pricing
logic touched.

### Added
- Auction4Cars and Dealer Auction purchases now feed the purchases page,
  reading each account's own real completed buy history (Auction4Cars'
  own 13 month "My Purchase History", Dealer Auction's own itemised
  Invoice history). Dealer Auction also gets a real full photo gallery,
  read off the won advert's own page since the invoice list itself
  carries none.
- A real DealerWay logo everywhere a platform logo already appears
  (the platform toggle, the purchases table's own Platform column, the
  card CTA), a fifth captured logo alongside Motorway, Carwow,
  Auction4Cars and Dealer Auction.
- A small link icon next to each purchase's own vehicle name, opening the
  original listing (with its photos) in a new tab wherever a listing url
  is known, the one way to see real photos for a platform with no
  gallery reader of its own.
- Automated daily logins for Auction4Cars, Dealer Auction and LE Capital,
  the same keychain based mechanism already covering Cazana, Motorway,
  Carwow and DealerKit, bringing every site this project reads to a
  self healing login.
- A Live Auctions/Purchases/Lost page switcher, a search icon, a
  settings cog and a sticky second filter row, all standardised across
  the cockpit, purchases and Didn't Win pages so the three read as one
  product rather than three separately evolved ones. The purchases
  table's own Platform column shows the real platform logos too, in
  place of the old coloured text pill.
- Comfortable/Compact switched from text to icons on the cockpit's own
  filter row, moved next to the Filters button.

### Changed
- The DealerKit push status on the purchases page is now a real
  red to green fraction badge (matching DealerKit's own job progress
  indicator) instead of a plain tick, showing exactly how many of the
  real checklist items are on record, not just none/some/all.
- Purchases and Didn't Win removed from the left sidebar: with the same
  three destinations already reachable from the new top nav page
  switcher, the nested links (and the down arrow that used to collapse
  them) had become a duplicate rather than a second real path.

## v3.1.4, 2026-08-26

A live progress bar on the purchases page for every sync and check, sortable
columns, and one small rule learned from a hide note. No gate or pricing
logic touched.

### Added
- A shared progress bar on the purchases page, matching the cockpit's own,
  shown for Sync now, Check all DealerKit records, and every per row check
  or push. It compares against the server's own clock rather than the
  browser's, so a genuinely stuck run past 90 seconds turns amber and says
  plainly "no update for Ns" instead of just spinning.
- Sortable columns on the purchases page: click any header to sort by it,
  click again to reverse, with an arrow marking the active column and any
  unset value always sorting to the end, whichever direction.
- "master" added to VAN_MODELS, learned from a hide note on a 2019 Renault
  Master tipper.

### Fixed
- The purchases page's Sync now progress bar could show a stale, unrelated
  message left over from an earlier daily run rather than the sync's own
  real progress, because purchases_run.py's own sync never wrote to the
  shared progress file. It now reports real progress through every stage,
  including a live count through the slowest step, the payment breakdown
  lookup per purchase.

## v3.1.3, 2026-08-26

A Due in view on the purchases page, and older purchases out of view by
default. No gate or pricing logic touched.

### Added
- A Due in view, next to the Stats button, showing every still active
  purchase (not yet collected): those with a real due in date today or
  later, sorted soonest first, then anything still active but not yet
  confirmed, clearly labelled rather than missing entirely. Works
  alongside the platform tabs, not instead of them.

### Changed
- The purchases page now starts filtered to purchases bought from 1
  June 2026 onwards, display only: the real records are untouched,
  clear the date box or click Reset to see everything again.

## v3.1.2, 2026-08-26

A cap on the resale figure with a manual override, Carwow's own due in
date, pushing it through to DealerKit, and the purchases page's Auc
Status column redesigned into a real 3 stage tracker. No gate or
pricing logic touched.

### Added
- A settings level cap on the Retail est. figure (Settings > Overview),
  optional, none by default: a purchase priced above it shows and
  pushes to DealerKit at the cap instead of the formula's own higher
  figure. A pencil button on the purchases page row lets you set the
  figure manually instead, always winning over the cap; the real,
  uncapped formula figure is kept underneath either way, never lost.
- Carwow's own "Collection confirmed" card, on a won car's own listing
  page, now feeds the Due in column the same way Motorway's delivery
  timeline already does.
- Due In / In-stock On, a real DealerKit field on the vehicle's own
  Miscellaneous edit panel, now gets BidBrain's own collection date
  pushed to it as a fifth pushable item alongside price, photos,
  documents and retail. Unlike Retail Price it always stays in sync
  with BidBrain's own latest figure rather than only ever being set
  once.
- The purchases page's Auc Status column is now a 3 stage progress
  tracker (Bid, Documents, Delivery) instead of a single status word
  with a dot: a £ icon for Bid (always shown reached, every row here is
  already a won purchase), a document icon for Documents, a car icon
  for Delivery, each green once reached, orange while known but not
  yet, grey when nothing has been read yet. The real underlying status
  word is still there as a hover tooltip.

### Fixed
- The Delivery stage icon was reading Motorway's own coarse purchases
  list status word, which does not always keep pace with its separate
  delivery timeline page (the actual source of the real due in date):
  a car with a real, already pushed due in date could still show
  Delivery as not yet arranged. It now trusts a known due in date
  outright, falling back to the status word only when no date has been
  read at all.

## v3.1.1, 2026-08-26

### Fixed
- A fresh install crashed opening the purchases or settings page before
  its very first run: reading the saved settings from a database that had
  never been initialised raised "no such table: settings" instead of
  reading as nothing saved yet. Present since theme colours started being
  read fresh at render time, and the same failure that had been turning
  the CI test run red on every push since. A brand new, never initialised
  database now reads the same as one with no settings saved.

## v3.1, 2026-08-26

Logins that look after themselves, a redesigned Integrations tab, and real
control over which connections run and which even show. No gate or pricing
logic touched.

### Added
- Automated daily logins for Carwow, Motorway and DealerKit, joining the
  Cazana one: each site logs itself back in on its own schedule (Carwow
  and DealerKit 17:00, Motorway 06:00, clear of everything else that
  touches its shared browser profile) from a credential stored once in
  the macOS keychain via `setup_credential.py <site>`, never in the repo
  or anywhere readable. An expired session now heals itself instead of
  silently breaking the next run.
- The Integrations settings tab redesigned into one flat, honest
  connections list: anything broken floats to the top under a banner
  naming it, every row carries a single plain language state (down since
  when and why, working and how fresh, or not set up here at all) instead
  of the old two separate signals that could contradict each other, an
  Auto or Manual badge shows which logins fix themselves and when, the
  explanatory prose sits behind an info button, and each automation's
  own switch applies instantly, no separate Save button. While a login
  window is open, a strip explains why every other login button is
  paused, and the other buttons visibly disable.
- Hide connections a dealership does not use. Hiding is more than
  cosmetic: it also switches that site's own automations off (a
  platform's "Read this platform" toggle, an integration's own enabled
  state), and showing it again switches them back on. Hidden connections
  stay in a muted section at the bottom of the list with a one click
  Show again. Cazana alone cannot be hidden, every run prices on it.
- The Run modal no longer shows a platform that is hidden or switched
  off in Settings at all, and re checks the live settings every time it
  opens, so a change applies immediately without waiting for the next
  run. A hidden site's own buttons (Sync LE Capital funding, Run Glass's
  checks, the DealerKit pair) vanish with it.
- "Read this platform", a real persistent per platform setting on each
  platform's own tab: switched off once, that auction house is excluded
  from the scheduled daily run and every manual run until switched back
  on, whatever the Run modal is sent.
- A personal card display preference: clicking any card's own flag notes
  or valuation detail arrow now expands that section on every card at
  once, and the choice survives a reload, per browser, never changing
  the shared default.
- The single theme colour split into four genuinely independent ones
  (background, price, pill, icon) plus the left panel colour, each with
  its own picker on the Theme tab.

### Fixed
- A dead Carwow session used to read as "0 auction cars today",
  indistinguishable from a genuinely quiet day; it now fails loudly with
  a clear relogin message and the run falls back to the last good
  Carwow cars instead of wiping them.
- The login status text and the health dot on the settings page could
  contradict each other (a red dot right next to "logged in within the
  last day", a green dot next to "no saved login yet"): a successful
  login, automated or through the page's own buttons, now clears a stale
  failure record, and the two signals are merged into one honest state.
- Logging in through the settings page's own "Log in to X" buttons never
  cleared a stale down flag, so a genuinely working login looked broken
  however many times it was redone.
- A purchase with any retail estimate on file, even a stale wrong one,
  was permanently skipped for a fresh Cazana lookup.
- Settings page reloads (hiding a connection, finishing a login, the
  software update) used to dump back on the Overview tab; the current
  tab now survives any reload.
- Motorway's automated login wrongly reported failure when the shared
  profile was already logged in.
- A merge conflict during the v3.0 redesign had silently emptied
  settings.html's own JavaScript entirely; caught and fixed before any
  release carried it.

### Changed
- Software update moved from the Integrations tab to the top of
  Overview, it is not a connection.
- "Click Run Glass's checks" prompts no longer nag when Glass's is
  switched off or hidden, pricing on Cazana alone is the configured
  state then.

## v3.0.1, 2026-08-26

The purchases page's own DealerKit column reworked into a real, honest
"what's actually there" check, plus a real image reliability fix. No gate
or pricing logic touched.

### Added
- Check DK record, per row and in bulk: a live read of what is REALLY on a
  car's own DealerKit record right now (price, auction fee, delivery,
  indemnity, photos, service history, V5, retail price, due in status),
  independent of whatever BidBrain itself last attempted to push. Auction
  fee/delivery/indemnity are read from the car's real expense line items;
  Service History and V5 are detected from the real per-document label or
  filename, wherever either survives.
- A tick box per pushable category (price, photos, documents) sits right
  where its own presence mark used to be, pre ticked to whatever is
  missing on DealerKit and present in BidBrain's own data, with a single
  Upload to DK button; a new Check Motorway/Check Carwow re reads one
  purchase straight from its own platform.
- A hard gate: DealerKit will never create a brand new Due In record with
  no mileage on file (it would let DealerKit guess the mileage itself).
  Only applies to a genuine first push; an already existing record is
  unaffected. The page asks before pushing anyway.
- Four distinct tick colours (blue ticked, amber present but not yet on
  DealerKit, green already confirmed there, red not confirmed at all)
  with a tool tip on every state, and DealerKit's own login health now
  shows a red or green flag on the settings page like every other
  platform.

### Fixed
- A car missing real DealerKit data could still show a false green "all
  done" tick, both from an earlier miscount and from BidBrain marking a
  category "pushed" whenever it was merely attempted, even when there
  was nothing real to push (a purchase with no local documents at all
  still got marked as if its documents had gone up); both fixed at the
  source, and the real rows already carrying a stale flag from an
  earlier run corrected directly.
- Motorway's own photos and documents went permanently unviewable after
  about an hour: their urls are signed with a real expiry, not a file
  format issue. They are now downloaded to a real local file the moment
  they are read, the same approach Carwow's own documents already used,
  closing the same exposure in the DealerKit push pipeline itself.
- The settings page's "I'm logged in, save it" button could get stuck
  disabled for the very site being logged into.

## v3.0, 2026-08-26

A full visual redesign: a new design system, built first as a standalone
mockup, brought into all four real pages (BidBrain's own live auction
cockpit, Purchases, Didn't win and Settings). No gate, pricing or CompMatch
logic changed anywhere in this release, every real feature and its own
behaviour (filters, sorts, valuations, runs) works exactly as it did, just
restyled and reorganised. Also carries a small handful of real features
that had shipped since v2.9 but were sitting unreleased.

### Added
- A persistent left app nav shared by all four pages: real links for
  BidBrain, Purchases and Didn't win (nested under BidBrain) and
  Settings, plus Dashboard/Stock/Customers/Reports shown as inert "Soon"
  placeholders for the future Dealer OS rather than dead links. Collapses
  to a 60px icon rail via a toggle in the logo row (remembered per
  browser, so a reload does not spring it back open), carries Right
  Drive's own real logo (the full wordmark expanded, the round smile
  mark alone collapsed, its own white background chroma-keyed to real
  transparency), and a down arrow on the BidBrain item collapses its own
  nested Purchases/Didn't win links.
- A new Theme tab in Settings: the primary brand colour and left panel
  background colour, plus real logo (full and small) and favicon
  uploads, validated and stored server side with no files written to
  disk, defaulting to Right Drive's own real logo when nothing is
  uploaded.
- The search box moved out of each page's floating Filters panel into
  the top nav bar itself, next to the refresh icon, on the three data
  pages (cockpit, purchases, didn't win); Didn't win's own now-empty
  Filters panel (search was its only field) was removed outright rather
  than left pointless.
- Purchases and Didn't win both gained a real heading matching the
  cockpit's own per-platform one: the platform's logo (or "All") plus a
  live count, with the old permanent summary tiles moved into a Stats
  dropdown button opened the same way the notification bell already is.
- A quiet, disabled placeholder calendar icon next to the refresh icon on
  cockpit, purchases and didn't win, matching the sidebar's own "Soon"
  treatment until there is a real calendar feature to wire it to.
- A real Cazana deep link: the "Cazana" label in a shortlist card's own
  Valuation detail, and a dedicated "Open Cazana lookup" button, both
  linking straight to that exact car's own live Percayso search result
  (captured at the moment of a genuine lookup, cache hits included), not
  just naming the source.
- A "Due in" (collection date) column on the purchases page, read off
  Motorway's own delivery timeline for the day transport actually
  collected the car, the real date DealerKit's own Tax Date field should
  carry, not the same thing as the "Delivered" date already shown
  elsewhere.

### Changed
- The whole colour and type system moved to the new design system's real
  oklch tokens and Plus Jakarta Sans, card radius and shadow softened, a
  subtle hover lift added; the filter sidebar (all four pages) became a
  floating panel opened from a Filters button rather than a permanent
  column, freeing the card grid back to multiple columns on a 14 inch
  laptop.
- Shortlist card photos are real 4:3 now instead of a fixed pixel height,
  and the grid's own per-card minimum was retuned so exactly 3 cards fit
  a 14 inch laptop's real width (up to 6 confirmed live on a 27 inch
  monitor).
- Every page's top right icon cluster now reads the same, left to right:
  refresh (Run now / Sync now), the shortlisted star, the notification
  bell, the signed in user's avatar, all icon-only with no bordered
  boxes; every remaining glyph or CSS pseudo-element icon (gallery
  controls, sort arrows, close/star/delete buttons) was replaced with a
  real inline SVG matching the nav's own feather style.
- The sidebar's own collapse toggle changed from a plain chevron to the
  standard "box with a left panel" icon and grew bigger, alongside a
  bigger Right Drive logo.
- The platform All/Motorway/Carwow/... tabs on every page now share the
  exact same grey the left panel itself uses, previously a slightly
  different or stale tone on three of the four pages.

### Fixed
- The top nav bar's own `position:sticky` had quietly never worked since
  the app shell was first built: `overflow:hidden` on the content panel
  (there to clip to its own rounded corners) breaks sticky positioning on
  anything inside it. Moved the rounding onto the bar's own top corners
  instead; it now genuinely stays pinned while the page scrolls
  underneath it.
- `daily_run.py`'s settings cache is correct for its own short lived CLI
  runs but was wrong for `serve.py`'s long running process, so a saved
  theme colour or logo only ever took effect on the very first render
  after the server started; the settings save handler now clears it
  immediately.
- Didn't win's own new heading/Stats/Filters styling had silently never
  landed on that page at all (the CSS edit was anchored to text only
  purchases.html happened to share); fixed directly against its own copy
  of the block.
- The red notification badges (the bell, the shortlisted star) were
  cutting the number off at the bottom of the circle: `box-sizing:
  border-box` shrinks the badge's own content area once its border is
  subtracted, but `line-height` was still sized for the full box.
  Switched to flex centring, the same mechanism every other icon button
  here already uses.

## v2.9, 2026-08-25

### Added
- Every page now has a "Shortlisted cars" star icon in its own nav bar,
  the same plain look as every other nav icon, gaining a small corner
  badge with a real count whenever there is at least one, the same
  convention the notification bell's own badge already uses. On the
  cockpit it's the existing star toggle; on purchases, Didn't win and
  settings it deep links to `cockpit.html?view=shortlist`, landing
  with the shortlist filter already active.
- The notification bell (previously cockpit only) now also appears on
  purchases, Didn't win and settings, fed live by a new `GET
  /api/notices` endpoint that recomputes the same health notices from
  the last saved run cache; the bell itself turns red whenever there
  is a real alert, not just its small badge count. A notice with a
  real fix still links straight to it (Settings > Integrations, or
  the cockpit's Run modal via a new `?openrun=1` deep link on pages
  with no Run modal of their own).
- The BidBrain logo in every page's nav bar now links back to the
  cockpit, the live auctions homepage.

### Changed
- Nav icons are now in the same fixed order and same look on every
  page: favorites, purchases, notifications, lost bids, settings,
  each page's own primary action button(s) first (Run now, Sync now,
  Save settings, all now sitting in the identical right aligned
  action group) and a Back to the cockpit icon last where relevant; a
  page skips its own icon (no settings icon on the settings page, and
  so on) rather than showing a dead link.
- The settings page's internal tab set (Overview, Motorway, Carwow,
  Auction4Cars, Dealer Auction, DealerWay, Integrations) now uses the
  same pill style as the cockpit's own platform tabs, and moved out of
  its own row into the top nav bar directly, sitting next to the
  brand exactly where the cockpit's tabs sit.
- Save settings and Save integrations moved from a sticky bottom bar
  into the top nav bar, next to Sync now and Run now's own position
  on the other pages.

## v2.8.19, 2026-08-25

### Changed
- The settings page's Integrations tab gained a dedicated Valuations
  card for Glass's and Cazana (previously split between the
  Integrations card and the Auction house card), and every login
  row's relogin buttons now sit to the right of its status text
  instead of stacked underneath it, using the available width better.

## v2.8.18, 2026-08-25

### Added
- The Didn't win page gained an All/Motorway/Carwow platform toggle,
  matching the cockpit and purchases page, and its Platform badge now
  reads the row's own real platform instead of being hardcoded to
  Motorway. Ready for a Carwow Didn't win reader whenever that gets
  built; the Carwow tab honestly shows no rows until then.

## v2.8.17, 2026-08-25

### Changed
- The purchases, Didn't win and settings pages' cross navigation links
  are now icons (bag, target, wrench, home), matching the cockpit's
  own nav buttons, instead of plain text links.

## v2.8.16, 2026-08-25

### Changed
- The Didn't win page's Retail est. now shows Cazana 115% and Margin
  (retail est. less our own bid) in a hover/tap popover, the same
  pattern the purchases page already uses, rather than a separate
  Cazana 115% column.

## v2.8.15, 2026-08-25

### Added
- The Didn't win page also shows a Cazana 115% column now, alongside
  Retail est., the same plain retail-times-1.15 figure purchases
  already store directly.

## v2.8.14, 2026-08-25

### Added
- The Didn't win page gained a Retail est. column and a new summary
  tile: the running average of the real sold-for price as a % of
  CompMatch's own Bias bid across every lost bid with both known, the
  same comparison the purchases page already shows.

## v2.8.13, 2026-08-25

### Changed
- The settings, purchases and Didn't win pages now share the cockpit's
  own nav bar and link directly to each other, so you can move between
  any of the four pages without detouring back through the cockpit
  first. The settings page's old dark topbar is gone, replaced with
  the same sticky control bar the other pages already use.

## v2.8.12, 2026-08-25

### Changed
- The Status column now shows the platform's own real status as the
  main text always (Collection arranged, Delivery requested, Checks
  complete...), with a hover/tap popover only when there's a payment
  breakdown still marked Estimated, showing both the status and the
  payment note together rather than stacking them and growing the row
  taller than the rest of the table.

## v2.8.11, 2026-08-25

### Added
- The purchases page's Winning bid popover now also shows the "Bias
  bid" (what CompMatch would suggest for that exact car today) and
  what % of it the real winning bid actually came to — under 100%
  means the car was won for less than CompMatch's own suggestion. A
  new summary tile shows the running average across every purchase
  with both figures known.

### Fixed
- Carwow's own purchase status came through as a raw, unformatted
  value ("collection_arranged"); both platforms' statuses now display
  in plain sentence case ("Collection arranged").

## v2.8.10, 2026-08-25

### Changed
- The purchases page is tidied up for small screens: from 17 columns
  down to 11. Winning bid's own fee breakdown (Fee, Transport, VAT,
  Total paid) and Retail est.'s own Cazana 115%/Margin both moved off
  their own columns into a small popover shown on hover, or a tap on a
  touch screen. Service history and V5 now each show only the first
  document photo with a small count badge, rather than a whole row of
  thumbnails; click still opens the full gallery.

## v2.8.9, 2026-08-25

### Fixed
- CompMatch's comps could be years old: every comp's own retail was
  looked up today while some sales happened up to 5 years ago, so
  depreciation was systematically inflating old comps' own % of retail
  (a 4-year-old sale counted as a full-price comp today). Comps older
  than 18 months (or with no readable date) are now excluded entirely,
  not just capped. Model grouping was also splitting real groups apart
  (BMW comps keyed on their own engine badge, "118D"/"320D", never
  matching a live "BMW 1 Series" car; Mercedes A-Class was split three
  ways) — both now merge correctly. Semi-automatic comps, previously
  stranded in a bucket too small to ever match anything, now pool with
  Automatic (manual stays completely separate, the hard gate is
  untouched). Net effect on the real list: coverage held (126 to 131
  suggestions), but with 24 stale-only ones honestly dropped and the
  102 kept moving a median -5.1% as inflated old comps came out.

## v2.8.8, 2026-08-25

### Added
- A vehicle's own Motorway page carries a real, confirmed transmission
  and engine size (checked live), so lost bids and Motorway purchases
  missing them can now be backfilled from a genuine source, not just a
  name badge or a recent sighting. Recovered 634 real confirmations in
  one pass (transmission coverage across the whole comp history rose
  from 180 to 814 of 1,038, including the first real "Manual"
  confirmations this project has ever had). With the transmission hard
  gate from v2.8.7, suggested cards rose from 13 back up to 119 of 458.

## v2.8.7, 2026-08-25

### Changed
- Manual and automatic are now a hard gate for the suggested bid, never
  blended together even when there is too little data of one kind: a
  car's own transmission is only ever compared against real comps of
  that exact same transmission, with no fallback to a mixed pool. Since
  "Manual" is essentially never a confirmed value anywhere in the
  historical data yet (only real automatics are currently identifiable,
  see v2.8.2/v2.8.3), this sharply reduces how many cards get a
  suggestion right now (from 209 to about 13 of 458), most cars are
  manual and currently have no confirmed manual comps to compare
  against. This closes as real transmission data accumulates from daily
  reads going forward.

## v2.8.6, 2026-08-25

### Fixed
- CompMatch's % of retail calculation now excludes any individual comp
  whose own winning price came to more than 105% of its own retail
  (almost certainly stale or mismatched historical data), rather than
  only capping the final figure afterward. Checked live: this bad data
  was giving a genuinely backwards signal for some models, making
  automatics look like they sold for LESS of retail than manuals, the
  opposite of reality. After excluding it, most models correctly show
  automatics selling for more. Cars needing the retail cap dropped from
  69 to 19 of 209 suggested cards.

### Added
- CompMatch now compares a car against other examples of the SAME
  model on the SAME transmission specifically, once there is enough of
  that model's own automatic (or manual) history, rather than folding
  transmission into a single blended similarity score. A cross-model
  comparison was tried and rejected, it mixed in unrelated models and
  gave a backwards signal.

## v2.8.5, 2026-08-25

### Fixed
- The suggested bid could recommend paying MORE than a car's own known
  retail value, found live on a real Ford Kuga (£10,807 suggested
  against an £8,769 retail): about a third of comps for some models show
  a historical winning price above their own retail, most likely stale
  Cazana lookups rather than genuine overpaying. The suggestion is now
  capped at this car's own retail value regardless of what produced the
  raw figure, and says so in its own tooltip when it applies. Affected
  about a third of all suggested cards.

## v2.8.4, 2026-08-25

### Added
- CompMatch can now learn from another dealership's own data too, when
  both sides agree. Each dealership exports its own comps (make/model,
  winning price, retail, mileage, age, transmission, engine size and
  grade only, never a registration, a price breakdown or a margin) to
  the shared repo via a new "Sync CompMatch data" button or
  `share_comps.py --sync`, and every suggested bid is pooled from both.
  Off by default, opt in by setting `DEALER_NAME` in your own
  `dealer_config.py`.

## v2.8.3, 2026-08-25

### Added
- CompMatch now also compares condition grade when ranking real comps,
  alongside mileage, age, transmission and engine size. Every car
  BidBrain reads each day now logs its grade too (mileage, engine and
  transmission were already added in v2.8), so a future lost bid or
  purchase is matched against real scraped data from day one rather
  than needing a derivation, no live re-read needed.

## v2.8.2, 2026-08-25

### Fixed
- The suggested bid (CompMatch) barely had any real age or transmission
  data to compare against: only 5 of 1,038 real comps carried a year and
  none carried a transmission, so most suggestions were quietly falling
  back to a plain "same make and model" comparison rather than the
  intended like for like one. Fixed by deriving a real year from the
  registration's own UK plate age identifier and an automatic badge from
  the platform's own vehicle name, both free, no lookup needed, closing
  the gap to 981 of 1,038 comps carrying a real year. Like for like
  comparisons now cover about 89% of suggested cards, up from 61%.

## v2.8.1, 2026-08-25

### Added
- The suggested bid to win is now scaled to this specific car's own
  retail value when enough real comps carry a retail figure of their
  own: each comp's winning price is expressed as a % of its own retail
  first, and that % is applied to this car's own retail, so two comps of
  different spec (and so different retail) are compared on how
  competitively each one actually sold, not treated as equally
  comparable raw prices. Falls back to a flat raw price comparison when
  this car's own retail isn't known yet or too few comps carry one.

## v2.8, 2026-08-25

### Added
- The suggested bid to win now compares like for like rather than just
  "same make and model": real comps are ranked by how close their own
  mileage, year, transmission and engine size are to the specific car
  being priced, using the closest matches once there is enough real data
  to rank on, falling back to the full model group otherwise. A card's
  own tooltip says which kind of comparison it got.
- Every car BidBrain reads each day (sightings, not just gate passers)
  now also logs its mileage, engine and transmission, so a later lost
  bid or purchase can be filled in for free from BidBrain's own database
  rather than a fresh live read. `bid_retail_enrich.py --fields` backfills
  year, transmission and engine onto existing lost bids and purchases
  this way; the mileage backfill checks the database first too, only
  falling back to a live Motorway read when nothing is on file.
- Purchases now store the real mileage already read off the platform's
  own purchase page (previously used once for a Cazana lookup, then
  discarded), so it is available for the suggested bid model with no
  extra page visit.

## v2.7.12, 2026-08-24

### Added
- A suggested bid to win, shown directly on a shortlist card's own
  gauge: a purple marker and figure learned from BidBrain's own real
  history, both won purchases and lost auctions (a lost auction's own
  real sold price is exactly as precise a data point as what we
  actually paid to win one). Shown only when there is enough real
  comparable history for that make and model (55 real model groups
  currently qualify), never a guess otherwise.

## v2.7.11, 2026-08-24

### Added
- A new "Didn't win" section: every real Motorway auction bid on and
  lost, with the real price it actually sold for, read off Motorway's
  own Bids & offers screen. Ready for a future suggested bid feature,
  the real winning price is a signal purchases alone never carry. A new
  nav icon on the cockpit opens it, alongside a search box, a Sync now
  button and a one off "Read full history" backfill (the whole real
  history, 931 real lost bids, was captured live). Scheduled daily
  alongside the purchases capture.

## v2.7.10, 2026-08-24

### Fixed
- Two real DealerKit value discrepancies found on older Carwow records
  (WG18EZE and HJ17ECW's own Buyers Premium, and HJ17ECW's own Delivery
  Charge) were corrected to BidBrain's own captured, confirmed correct
  figures, replacing the wrong ones rather than leaving them.
- A genuine duplicate expense on WX16AUE (a whole standalone extra
  Delivery Charge record, left over from however the older stock was
  first entered) was removed, leaving one clean, correct expense.
- `add_missing_expense_items` could silently fail to open a supplier's
  own expense at all when that expense's row happened to sit at the
  very bottom of DealerKit's own scrollable expenses list, its own real
  edit button present in the page but effectively unclickable off
  screen. The matched row now scrolls into view first.

### Added
- New `delete_expense` in the DealerKit write module, for removing a
  whole duplicate expense record via its own real "More" menu (distinct
  from a single line item's own delete, which only clears client side
  until an expense keeps at least one item left to save).

## v2.7.9, 2026-08-24

### Fixed
- Pushing a purchase to DealerKit could create a genuine duplicate "Due
  In" vehicle record if that reg was already in DealerKit as a completed,
  disposed sale: DealerKit's own creation endpoint only refuses a repeat
  reg still active in some other stage, not one already sold. A reg is
  now looked up in DealerKit first, before ever attempting to create
  anything, closing the gap regardless of the existing record's status.
  13 real duplicates created by an earlier batch push were found and
  removed; the original sold records were confirmed untouched throughout.

### Added
- All 23 Carwow purchases are now fully pushed to DealerKit: purchase
  price, delivery fee, buyers premium, service history and V5, matching
  what Motorway's purchases already had. A handful of older records
  already carried some of these figures from before, entered by hand;
  those were never overwritten, only genuinely missing pieces were added.

## v2.7.8, 2026-08-24

### Added
- The purchases page's "Sync now" button now opens a modal to sync
  Motorway and Carwow together or individually, the same shape as the
  cockpit's own Run modal, ready for later auction houses to join the
  same way once they feed the purchases table too.
- Each purchase row gained a DealerKit push status button: grey "Push
  to DK" until that car has been pushed, green "Synced" once it has.
  Clicking it either way pushes just that one vehicle, so a single car
  can be re pushed without running the full batch.

## v2.7.7, 2026-08-24

### Fixed
- Carwow's own service history documents were being captured and saved
  correctly, but never shown: the Service history column only ever read
  Motorway's own url based field, ignoring Carwow's (a list of local
  file paths on this Mac). Both platforms now show real clickable
  thumbnails in that column, and the V5 column does the same instead of
  just a page count, now that a local path is known to be servable
  directly from this page.
- 13 of 23 real Carwow purchases also had their service history list
  polluted with V5 pages that had leaked in from an earlier version of
  the capture logic, before the two were split into separate fields.
  Cleaned directly in the database; the current capture code already
  keeps them apart correctly going forward.

## v2.7.6, 2026-08-24

### Fixed
- The purchases table still forced a horizontal scrollbar on a normal
  wide monitor even after v2.7.5's own nav bar work, because the page's
  own content width was capped well below what a real monitor could
  show, and one column (a car's service history thumbnails) could grow
  without limit. The width cap is now generous enough to use a real
  monitor's own space, and that one column scrolls sideways within its
  own small cell instead of stretching the whole table. Verified live:
  no scrollbar at 1920px with all 16 columns visible, and a narrower
  window still scrolls the table within its own frame rather than the
  whole page, same as before.

## v2.7.5, 2026-08-24

### Fixed
- A purchase's Retail est. on the purchases page could silently come
  from two different formulas depending on whether BidBrain happened
  to have a recent buying assessment on file for that reg: usually the
  intended Cazana based dealer pricing formula, but sometimes the
  buying brain's own governing value instead, a different figure meant
  for a bid ceiling, not a resale price. Found live on two real
  purchases (neither had a Cazana figure to show, and their retail
  estimate didn't match any real dealer price ending). Now always
  computed the same way, from a live Cazana lookup.

### Changed
- The purchases page's nav bar now matches the cockpit's own single
  control bar (brand, platform tabs and actions all in one sticky
  row), replacing the old stacked dark topbar plus a separate tabs
  row, freeing up vertical space and keeping the two pages consistent.
- Added a search box (reg, make or model) and a bought date range to
  the purchases page's own control bar.
- The rest of the purchases page is now responsive: the control bar's
  filters and actions reflow on a narrow screen, and the summary tiles
  and table gain extra breathing room down to a phone's own width (the
  table itself already scrolled sideways within its own frame, never
  the whole page).

## v2.7.4, 2026-08-24

### Added
- Carwow purchases now get mileage and a Cazana based retail estimate,
  the same logic Motorway's own purchases already used. V5 is now its
  own column on the purchases page and its own database field,
  separated out from the service history documents it used to be
  bundled with, ready for a future DealerKit document type mapping.
- A "Sync now" button on the purchases page, running the same check
  the daily schedule does on demand. The daily purchases check itself
  is now genuinely scheduled for the first time on this install, at
  3:45pm.

## v2.7.3, 2026-08-24

### Fixed
- Carwow's own payment breakdown was only reading correctly for the
  most recent purchases. A fully paid sale relabels "Vehicle price" to
  "Final vehicle price" (now matched too), and an old enough purchase
  drops the itemised breakdown entirely, only a plain "Sale agreed for
  £X" headline survives (now the fallback, price only, fees left
  honestly unknown rather than guessed). All 23 real current purchases
  now have a real price.

## v2.7.2, 2026-08-24

### Fixed
- Purchase page thumbnails were missing for any purchase that only ever
  had a photo gallery captured and no separate single photo (Carwow's
  own purchases, which don't set that second field the way Motorway's
  do). The thumbnail now falls back to the gallery's own first photo.

## v2.7.1, 2026-08-24

### Fixed
- Carwow's own won listings screen was only ever reading 3 of the real
  cards. Three separate causes: each card is a lazy loaded turbo-frame
  that only fetches its own content once scrolled into view (most were
  never triggered), the per card text was truncated at a length too
  short for some real cards, and an old suffix style plate format was
  one character too long for the reg pattern. Also widened the read to
  the page's own 90 day maximum, it was defaulting to 30. Verified
  live: now reads every real deal, matching the site's own total
  exactly.

## v2.7, 2026-08-24

### Added
- Carwow won purchases now capture everything Motorway's already did: a
  real itemised payment breakdown (vehicle price, Carwow's own buyer
  fee, transport fee, VAT), the full photo gallery, and the seller's
  service record and V5C logbook scans, downloaded from Carwow's own
  document bundle and pushed into DealerKit the same way Motorway's
  already are. A purchase still mid document review on Carwow's own
  side correctly captures nothing yet rather than guessing.

## v2.6.2, 2026-08-24

### Fixed
- DealerWay's own read window only ever captured auctions ending today
  or tomorrow, silently dropping real live auctions running further out
  (its lots run several days, the same as Auction4Cars, which had
  already been fixed away from this exact narrow window). Widened to 7
  days with a proper inclusive range check. A live check found real
  cars within a normal search radius were being dropped before they
  ever reached the gate.

## v2.6.1, 2026-08-24

### Added
- A cockpit price ceiling on the settings page's Overview tab (Settings
  > Cockpit display). When set, it pre fills the cockpit's own Max bid
  and Reserve sidebar filters with the same figure on every page load,
  hiding cars priced or reserved above it by default. Display only,
  never a gate: pricing, gating and every count are untouched, and
  clearing either box, or clicking Reset, shows everything again for
  that session.

## v2.6, 2026-08-24

### Added
- A "Software update" card on the settings page's Integrations tab,
  showing the current and latest released version. When a newer one
  exists, an "Update now" button fetches tags, checks out the release,
  and restarts the server so it takes effect, the same as running the
  update commands and a `launchctl kickstart` by hand. Refuses while a
  run is in progress or the working tree has local changes, rather than
  risk a checkout over something real.
- The daily job is genuinely scheduled for the first time on this
  install: two staggered launchd jobs matching Motorway's and Carwow's
  own different daily stock times, Motorway alone shortly after 4:30pm,
  everything else shortly after 5pm.

## v2.5, 2026-08-24

### Added
- DealerWay (dealerway.co.uk) added as a fifth auction platform, built
  against real captured pages: a real NAMA style condition grade and
  exact distance, a live per lot countdown, and three disclosed issue
  flags (test driven, bodywork, mechanical/electrical) read straight off
  each card. It discloses no reserve, so Current Bid, or Starting Price
  before any bid, stands in, the same workaround already used for
  Auction4Cars and Dealer Auction. A combined mechanical/electrical
  disclosure is a hard reject; a disclosed bodywork issue is a card flag,
  never a rejection.
- The daily job is genuinely scheduled for the first time on this
  install: two staggered launchd jobs rather than one, matching
  Motorway's and Carwow's own different daily stock times. Motorway
  alone fires shortly after 4:30pm; everything else (Carwow,
  Auction4Cars, Dealer Auction, DealerWay) fires shortly after 5pm,
  deliberately excluding Motorway so it is never needlessly re read half
  an hour after it was already fresh.
- Cazana joins the settings page's relogin mechanism (Log in to X button,
  live session age shown on the Integrations tab), previously the one
  valuation source only ever re logged in from the terminal.

### Changed
- Each selected platform is now read, gated, valued and written to the
  cockpit in its own turn, rather than the whole run reading every
  platform first and only ever saving once at the very end. A finished
  platform's cars go live immediately rather than waiting on a slower
  one, and a later failure on one platform (a dead Cazana login mid
  Carwow, for example) no longer wipes out an earlier platform's own
  already saved results the way a single end of run write always used
  to.
- A car's own detail page (owners, service history, disclosed issues,
  photos, and on Carwow/Auction4Cars the reserve itself) is now cached
  for 4 hours, keyed per car. Auction4Cars, Carwow and DealerWay auctions
  run several days, so the same car legitimately reappears run after
  run; a re seen car within the window skips its own detail page visit
  entirely rather than being re read from scratch every time.
- Motorway's own CSV export already discloses a seller reported
  mechanical issue as a plain column, never read before now. A car
  flagged there skips its detail page visit outright (a known issue is a
  hard reject regardless of anything else the page might reveal), a
  small but genuinely free speed win on top of the cache above.
- Bell notifications are now clickable straight through to whatever
  fixes them (a login notice opens the right settings tab, a "run this
  check" notice opens the Run modal), and the same red flag now also
  shows next to the affected site's own heading on the settings page
  itself, not just in the bell.

### Fixed
- The per candidate detail page read for Carwow, Auction4Cars and
  DealerWay had no progress reporting at all, so a genuinely slow but
  working multi minute pass looked identical to a frozen browser on the
  cockpit's own progress bar. All three now report progress per car.

## v2.4.2, 2026-08-24

### Added
- A real notice on the cockpit (a red dot on Run now, and the bell
  dropdown) when a platform's own login has genuinely failed in the last
  run: Motorway, Carwow, Auction4Cars, Dealer Auction or LE Capital.
  Never a background check, only ever reports what a run that was
  already happening found, so it carries none of the extra session risk
  a timer based poll would (Glass's single account wide session has
  shown before that even just checking it can evict the real one). The
  notice clears itself the moment that site reads cleanly again.
- Bell notifications are now clickable straight through to whatever fixes
  them: a login notice opens the settings page's Integrations tab
  directly, a "run this check" notice opens the Run modal. The same red
  flag now also shows next to the affected site's own heading on the
  Integrations tab itself, not just in the bell.

### Fixed
- `settings.html` and `purchases.html` 404'd on any URL with a query
  string attached (for example the new deep link to a specific settings
  tab), since the server matched the address exactly rather than just
  the page name.

## v2.4.1, 2026-08-24

### Added
- The relogin button introduced in v2.4 now covers all seven sites, not
  just the three back office automations: Motorway, Carwow, Auction4Cars
  and Dealer Auction each get one too, in a new Auction house logins card
  on the settings page's Integrations tab. No on/off switch for these
  four, that already lives elsewhere.

### Changed
- DealerKit's two automations (stock read, purchases push) now share one
  login block under a single DealerKit heading, instead of each showing
  its own duplicate login button.

## v2.4, 2026-08-23 to 2026-08-24

### Added
- LE Capital funding sync: reads LE Capital's own Current Stock and Stock
  History and adds or settles DealerKit's Finance > Funding records to
  match, LE Capital always treated as the source of truth. A value
  disagreement on a car live on both sides is only ever reported, never
  changed automatically. Runs from the cockpit's Run modal or
  `python3 daily_run.py --lecapital-funding`.
- A Settings page Integrations tab: DealerKit stock read, DealerKit
  purchases push, Glass's on demand checks and LE Capital funding sync can
  each be switched on or off independently. Turning one off disables its
  Run modal button, refuses it if started directly, and (DealerKit stock
  only) skips it in the scheduled daily run too.
- A relogin button under each integration, opening a real login window on
  the Mac and saving the session once confirmed, so a stale or missing
  login can be fixed from the settings page rather than a terminal
  command.
- Delivery report PDFs (Motorway's own vehicle condition and delivery
  photos) pushed into DealerKit as a document for every reachable
  purchased car.
- DealerKit's Tax Date corrected to each purchase's real Motorway
  collection date, the figure Xero actually uses for when payment was
  made.

### Fixed
- All 95 Motorway sourced DealerKit purchase records audited for correct
  and complete purchase price, delivery fee, buyers premium and indemnity
  fee, service history documents and V5s; genuine gaps found and filled,
  including 16 older purchases recovered via a Motorway purchase search
  this project had not used before.
- A car's full funding or expense picture was sometimes read from only
  the one expense record linked to its purchase, missing real fees filed
  under a separate supplier; now reads every expense on the vehicle.
- V5 document scans saved as .PNG rather than .jpg were silently never
  matched (only .jpg was ever recognised), dropping real V5s that were
  actually available; fixed and backfilled across every tracked purchase.
- Document filenames pushed to DealerKit were random rather than the
  source's own real filename, and a batch of several large documents
  could silently fail to save; both fixed.

## v2.3, 2026-08-21 to 2026-08-23

### Added
- Purchases can now be pushed straight into DealerKit as real Due In stock
  records: registration, purchase price, exterior photos, service history
  documents and V5C document scans, all from one on demand pass reachable
  from the cockpit's Run modal.
- Every purchased car now has its service history document photos and its
  V5C document photos captured and stored, and the full listing photo
  gallery (not just the one card photo) is captured for both the cockpit's
  shortlist cards and the purchases page, viewable in a shared lightbox
  modal on either page.
- Settings split into an Overview tab (the rules shared across every
  auction house) plus a tab per platform, with a reference only dealer
  postcode field on the Overview for documenting what should be set on
  each platform's own account.
- A guide for bringing a second installation up to date with the shared
  repo, and an automated login for a session that dies unexpectedly using
  a credential saved in the macOS Passwords app.

### Fixed
- Dealer Auction cards sometimes picked up a network badge icon instead of
  the real vehicle photo, and its own logo was missing a white variant so
  the "View on" button fell back to plain text.
- DealerKit's own Add Expense screen fails silently, no error shown at
  all, if its required Team field is left blank; every expense entered
  through this project's own automation now sets it explicitly.

## v2.2, 2026-08-21

### Added
- Real versioned Releases on GitHub, tied to this changelog: each release's
  notes are pulled from its matching section here.
- The full test suite now runs in CI on every push and pull request.
- The purchases report's payment breakdown now recognises Motorway's
  "Estimated breakdown" state (before a car is actually paid for), not just
  the confirmed one, and refreshes until the real figure lands.
- The purchases report gained a photo and a status column per row.
- An "Ends within" filter (today, tomorrow, 3 to 7 days) for Auction4Cars
  and Dealer Auction, the two platforms whose auctions run over several
  days rather than closing at a single fixed time.
- An "Ending soonest" sort next to the Cazana price sort; Auction4Cars and
  Dealer Auction default to it automatically when no other sort is chosen.
- The live countdown on a lot now ticks by the second once under an hour,
  not just every 30 seconds at minute granularity.
- A card disappears from view the moment its own auction ends, rather than
  sitting there greyed out with "Auction ended".

### Changed
- The purchases report's "Room" column is now "Margin", calculated from
  the winning bid rather than the full amount paid.
- Auction4Cars' reader now captures lots up to 7 days out (the longest a
  real auction there runs), not just today or tomorrow, so the new "Ends
  within" filter has real data to filter across.
- New installer setup now points at the latest release tag, not the
  live `main` branch, which moves as work in progress.

### Fixed
- A platform whose live read failed outright (a dead login, a timeout)
  was silently wiped from the shortlist instead of falling back to its
  own last known good cars; found live when Auction4Cars' session died
  mid session, now falls back with a loud message instead.
- A test asserting a hardcoded UK wall clock time against a raw epoch
  timestamp only ever passed on a machine set to UK time; caught by the
  new CI run defaulting to UTC, fixed to compare against the same
  timezone-dependent conversion the code itself performs.

## v2.1, 2026-08-20 (evening) to 2026-08-21

### Added
- Dealer Auction added as a fourth platform (Auto Trader's trade auction
  network). Auction listings only, no API used, same as every other source.
- A Run modal: refresh a single auction house at a time instead of always
  re reading all four, so a settings change to one platform doesn't force a
  full re read of the rest. The on demand Glass's and DMS stock buttons
  moved into the same modal.
- A purchases admin page: every car actually bought, its real payment
  breakdown (winning bid, platform fees, transport, VAT), a retail
  estimate, and a Margin figure against the winning bid, each row carrying
  the car's own photo and its current status.
- Saved, starrable filter views, and a live countdown to Auction4Cars lots'
  own end time (its auctions run 3, 5 or 7 days, not a single daily close
  like the other platforms).
- A traffic light system (red, amber, green) with topic relevant icons for
  the card insight flags, collapsed to a hoverable icon row with a chevron
  to expand the full text.
- A responsive shortlist grid, scaling from 2 columns on a laptop up to 6 on
  a 34 inch monitor, and a Reset link to clear every sidebar filter at once.

### Changed
- Sort and the shortlist view filter merged into one dropdown per platform
  section, alongside the platform's own logo and live car count.
- The "Best opportunity" sort now ranks every car on its Cazana value
  alone, so two cars priced from different valuation sources still compare
  fairly.
- Carwow's stand in reserve figure is now labelled "Trade value", not
  "Reserve" (Carwow never actually discloses a real reserve, this figure is
  CAP Clean standing in for one).
- The purchases report's "Room" column is now "Margin", calculated from
  the winning bid rather than the full amount paid, since fees, transport
  and VAT are cost of sale, not part of what the car itself made.

### Fixed
- Motorway's own payment breakdown has three states, not two: a genuine
  "Estimated breakdown" while a car is bought but not yet paid for was
  being silently discarded because only the final confirmed state was
  recognised, hiding real figures Mark could see by hand. Estimated
  figures now keep refreshing on each read until the confirmed ones land,
  then lock for good.
- Motorway purchases were showing the literal word "Delivered" as the
  vehicle name, and had no bought date at all, caused by an extra status
  badge on delivered cars' rows and a date parser that only understood
  relative phrasing ("3 days ago"), missing the absolute date format
  Motorway also uses.
- Auction4Cars' reserve was being estimated from a lower guide value; it
  actually publishes a real reserve in an inline script on each car's own
  page, now read directly instead of estimated.
- Make/Model filtering treated differently spelled makes from different
  platforms (for example "Mercedes" and "Mercedes-Benz") as separate
  makes, splitting one manufacturer's cars across several filter options.

## v2, 2026-08-19 to 2026-08-20 (early hours)

### Added
- Multi dealership support: dealer specific settings (a platform's saved
  search or filter id, the accounts email) pulled out of tracked source
  into a local, gitignored `dealer_config.py`, so this repo can be shared
  across more than one dealership without one dealer's account settings
  leaking into or overwriting another's.
- Auction4Cars added as a third platform, alongside Motorway and Carwow.
- A cockpit settings page: the hard gate (mileage, reserve, age, distance,
  condition grade, owners), the banned makes, models and value caps, and
  later the pricing formula itself (retail uplift, flat spread), all
  editable without touching code, and settable separately per platform
  since not every source discloses the same details.
- A DMS stock read (DealerKit) feeding the shortlist's "fills a gap /
  already have some" flags, replacing the previous dealer's own DMS
  reader for a dealership that uses a different system.
- On demand refresh buttons and a notification bell surfacing platform
  health issues (a dead login, a stale stock read) instead of a permanent
  row of banners across the top of the page.
- Reserve, max bid and retail guide range filters, plus a sort direction
  toggle for the shortlist.

### Changed
- The retail valuation cap that forced a car's governing value down when
  only one valuation source read high was removed at the new dealer's
  request; a valuation source now prices at its own full calculated value
  with no ceiling.

### Fixed
- A login detector was treating a platform's own sign in page as "logged
  in" because its URL happened not to contain the word the check looked
  for, capturing a dead session before any real login had happened.
- A hardcoded postcode in a gate failure message that was wrong for any
  dealer other than the very first one this was built for.
- A malformed saved filter URL that silently built a nonsense query
  parameter instead of a real one.
- Real dealer values (a saved filter id, an accounts email, a DMS
  subdomain) that had leaked into this shared changelog's own prose,
  defeating the point of keeping that configuration local to each dealer.

## v1, original build

The original build, for the first dealership this was written for, before
this repository's git history began. Not represented as individual commits
here since it predates this repo; the full decision log for this era,
including every rule and edge case behind it, lives in `CLAUDE.md`.

### Added
- The buying brain (`bidbrain/pricing.py`): the hard gate, the banned
  engine, make and model rules, and the core pricing rule (a retail uplift
  added to two valuation sources, a governing value threshold deciding
  which source counts, and a flat spread subtracted to get the
  recommended maximum bid).
- Live readers for two auction platforms' dealer stock and two retail
  valuation sources, all read from logged in screens, no platform APIs.
- The cockpit page: a daily shortlist with star and hide controls,
  valuation detail, a repeat appearance flag for cars seen before, and a
  reserve drop flag.
- A DMS stock and sales report reader, feeding "fills a gap" flags and a
  monthly learning loop (days to sell, prep cost and margin, per model).
- A private web address for the cockpit, reachable from any device.
- Scheduled daily and monthly runs, unattended.
