# BidBrain project status, part 18: The purchases module rebuilt as a working list (27 August 2026)

Moved unchanged from CLAUDE.md on 2026-09-18. The standing rules stay in CLAUDE.md, which lists every part of this log.

  - THE PURCHASES MODULE REBUILT AS A WORKING LIST, 2026-08-27 (Steven's
    own build, the big one from the Gemini meeting task list: "rebuild
    the purchases module UI", designed across several mockup rounds he
    signed off one by one, then "lets build it"). The purchases page is
    no longer a passive report of what the platforms said, it is the
    live working list for every car between winning the bid and landing
    on site, one table, Airtable style inline editing, a resizable
    record drawer, a real per car timeline, chips, cancellations and a
    journey with a Check in that archives the car. Built on Fable 5 per
    the model recommendation for this scale of change. NOT yet
    committed or pushed, per Steven's own release discipline, and MARK
    MUST REPEAT THE DATA GROUNDWORK ON HIS OWN MAC when this lands (see
    the memory file purchases-rebuild-mark-handover.md and the Mark
    notes below).
    - THE DATA GROUNDWORK, applied to Steven's live database first
      (backups taken: bidbrain.db.backup-20260827-163058, -prebid,
      -phaseA): 88 platform cancelled sales flagged per purchase (not
      reg keyed, see the Puma below), 104 old finished cars moved to
      stock_status "In stock" (the archive), leaving his real 21 car
      working list; the BMW X1 corrupted row fixed (SD66XWV, real reg
      and name recovered); the Ford Puma double sale resolved (listed
      and won on BOTH platforms the same day, the Motorway one
      cancelled, the Carwow one live, which is exactly why cancellation
      is now recorded on the PURCHASE ROW, cancelled_at/reason/source,
      never derived from the reg keyed walked_away table, whose reg
      only identity cannot say WHICH sale died); 165 missing Motorway
      winning bids recovered by inverting the fee band arithmetic
      (all_in = bid + fee(bid) x 1.2, the recovery self verified
      against all 30 rows that carried both figures before any write,
      30 of 30 exact, SA18BSO honestly left empty as unrecoverable,
      recovered rows written payment_estimated=1 so a later real
      breakdown read still replaces them); GBP 64,542 that had been
      sitting inside "price" reclassified as fees; the CompMatch comp
      inflation this caused corrected (about GBP 390 per car on 116D,
      A Class, Qashqai comps).
    - CANONICAL MONEY, ONE FUNNEL: db.list_purchases now derives every
      money figure once, per row: bid_original (winning_bid, else the
      platform price, else None for a Motorway row with no breakdown,
      NEVER the all in figure standing in for a bid), chip_total,
      car_price (bid less chips), effective_fee/effective_vat (the
      last chip's frozen re band, else as read), fees_total, all_in
      (car_price plus fees, falling back to the platform's own total
      for an old row). The page and the DealerKit push only ever read
      these, never re derive from raw columns.
    - CHIPS ARE FROZEN EVENTS, NEVER LIVE ARITHMETIC: purchase_chips
      stores amount, reason, bid_before, and the new fee and VAT
      computed ONCE at chip time (serve._record_chip, against the
      db.fee_for band tables) and frozen, never recomputed against a
      table that may have changed. Motorway re bands (Right Drive is
      on Motorway Pay and Instant Billing), Carwow never does, fees
      follow the ORIGINAL price there (Steven's final confirmation,
      matches FE68CKV's real invoice), both per dealership settings
      (purchase_flow.fee_rebands). Chips lock the payment field block
      in record_purchases so a later platform sync can never overwrite
      a chipped price back to the original; a platform read that
      MATCHES the chipped figure logs a one time timeline notice
      instead. Multiple chips per car stack; the platform's locked
      price is the ONE piece of locked platform data a chip is allowed
      to move, automatically, shown in the timeline as original to new
      (Steven's explicit design).
    - THE TIMELINE: purchase_events, one row per change, through a
      single mutation funnel (db.set_purchase_field with its editable
      whitelist, record_chip, cancel_purchase, check_in_purchase,
      add_purchase_comment), every entry stamped with who and when,
      comments included (Steven: "every change to any vehicle record
      should be shown in the timeline and timestamped including all
      comments and status changes"). The page has no private write
      path, every edit goes through /api/set-purchase-field, which
      also refuses a dropdown value that is not one of the configured
      options.
    - THE PAGE (render_purchases, rebuilt): four views (Due in, the
      working list, everything not In stock and not cancelled, default
      sorted soonest collection first; In stock, the archive, checking
      in IS archiving; Cancelled, every fallen through sale kept
      viewable with its reason; All), independent of the platform
      tabs. The old June 2026 date pre fill is gone, the views make it
      redundant. Columns per Steven's Airtable: Status, Video, Person
      collecting, Paid for, On finance, Notes (renamed from Due in
      status, his ask), Collection arranged, Location, Car price,
      Chipped, plus the existing Retail est., Service history, Logbook
      (V5 scans plus a logbook_state pill in one cell), DealerKit.
      Every column sortable (SORT_KEYS extended, each key returns
      exactly what its column displays), every column hideable per
      Table and per Record separately via the Fields menu, saved
      server side (purchase_columns) so it holds on every device.
      Inline editors: a floating option menu for dropdown columns, a
      floating input for text, number and datetime, both committing
      through saveField. Reg and Vehicle open the record drawer, no
      row numbers, no vertical separators (his ask). An Add car by
      hand modal for a car bought outside the platforms (a local
      trader), platform "Manual", nothing ever overwrites it, marked
      with an M tag.
    - THE RECORD DRAWER, the approved journey header design (his
      explicit mid build instruction that it must be in the build):
      the stepper above the tabs (Bought, Video check when
      purchase_flow.video_stage is on, Steven yes Mark off, Collection
      which Paid for closes, Arrived which Check in closes), a Next up
      card saying the one thing to do next, collapsible stage cards
      with their own editable rows, tabs Car details / Timeline /
      DealerKit, an always visible comments box, a drag handle whose
      width persists per device (localStorage bb_drawer_w), and a
      three dot actions menu holding Record a chip (any point, more
      than once) and Cancel this sale (any point, reason type from the
      configured list plus required free text, kept for the proof
      pack). The video stage closes on the LAST option of the
      dealership's own video_status list, so the order in settings is
      meaningful, not just cosmetic. The DealerKit tab is a read only
      summary of the last Check DK record including a price mismatch
      warning when DealerKit's stored purchase price disagrees with
      the canonical car_price (a chip moves BidBrain, never DealerKit,
      on its own).
    - MILEAGE HARD BLOCK, NO ESCAPE HATCH ANYWHERE (Steven: "NEVER
      ALLOW Dealerkit to manually guess the mileage"): the old
      allow_missing_mileage confirm-through flow is REMOVED from
      dealerkit_write, serve.py, purchases_run.py, daily_run's bulk
      pass and the page (the Upload button is disabled outright with
      "Enter the mileage first"); a car with no mileage can never
      create a DealerKit record from any path.
    - SETTINGS: a new Purchases tab on the settings page edits every
      dropdown column's own answers (add, reorder with real up/down,
      recolour, flag default/finished/retired, remove) plus the
      journey flags (video stage on/off, fee re bands per platform).
      Saves through its own /api/purchase-page-settings (the full
      /api/settings route needs the whole buying rules payload this
      page never holds). A REAL IN USE GUARD server side: removing an
      answer still carried by real cars is refused naming the count
      ("'Due in' is still on 109 car(s)"), retire it instead, a
      retired answer stays readable on old cars and is never offered
      again (openSelMenu filters them, videoDone ignores them). Fee
      band tables themselves stay code only, deliberately, they only
      change when a platform publishes new ones. Steven's own seeded
      config: person_collecting Relay Network/Carl/Collected in house,
      video order Awaiting confirmation, More info needed, Video
      requested, Video received, Video signed off (signed off is the
      gate), location/aucstatus/duein columns off, video_stage on,
      rebands Motorway yes Carwow no.
    - BUGS CAUGHT BUILDING IT: the settings panel's save button was
      wired with a parse time addEventListener but lives inside the
      cloned form template, so it was null at parse time and the whole
      settings page stuck on Loading (the exact template-clone timing
      class of bug this page has had before), fixed with an inline
      onclick; a mangled ternary in the chip cell caught before it
      ever rendered; the settings page needed its own copy of the pill
      palette (it shares no stylesheet with the purchases page), hex
      for hex identical.
    - VERIFIED LIVE END TO END in a real browser against Steven's real
      data, not just unit tests: the 21 car working list, drawer open
      on the real BMW X1, video status picked from his real options
      (Next up updated, table pill updated, timeline logged the
      change), a comment posted (landed in Comments AND the timeline),
      a chip recorded (live preview showed 7,500 to 7,250 with the
      Carwow fees-follow-original hint, header re read "after GBP 250
      chipped"), a collection date committed through the floating
      editor (the car re sorted to the top of the working list), In
      stock view showed exactly the 104 archived cars and Cancelled
      exactly the 88, the Fields menu round tripped a column off and
      back on (confirmed in the database both ways), the Stats
      dropdown read the canonical figures (21 showing, GBP 141,649.40
      all in, chip and cancellation rollups), and the in use guard
      refused a real removal. EVERY test artifact from that browser
      pass (the chip, the comment, the video status, the collection
      date, their timeline events) was then removed from the real
      database and verified gone, Steven's data is exactly as it was.
      600 tests pass (the ~30 new Phase A checks included), node
      --check clean on both generated pages, no leftover template
      tokens.
    - DRAWER SPACING, "EVERYTHING ELSE" AND WIDE WIDTH RESPONSIVENESS
      REDESIGNED, same day, straight after (Steven: "there are some
      clear spacing issues on the car overlay... the everything else
      section needs re thought out, it looks like its part of the car
      journey at the moment, and its not... when you make it bigger it
      creates too much space in width between the title and the data").
      Looked at the real rendered drawer at 940px before changing
      anything, which surfaced a genuine BUG beyond the spacing: the
      header's three dot actions button was absolutely positioned
      inside an inline span wrapper, so its right offset resolved
      against the zero size span rather than the header and the button
      rendered stranded under the platform pill. Header rebuilt as a
      real flex layout (identity row: reg plate, platform pill, manual
      tag, actions pushed right; the vehicle name on its own full
      width line beneath), no absolute positioning at all.
      The other three fixes, all live verified at 520px and 940px:
      (1) Field rows (.sgr) changed from space-between (which flung
      the value to the far edge on a wide drawer, the exact complaint)
      to a fixed label column grid, the value always sits right next
      to its label. (2) A CSS container query on the DRAWER's own
      width (container-type inline-size, not a viewport media query,
      since the drawer is resized by dragging): past 640px the stage
      body and section rows lay out two up so the extra width carries
      data instead of stretching one thin column, with the money
      block, notes, the closing row and the Check in button spanning
      both. The stepper is capped at 560px and centred so a wide
      drawer keeps it composed. (3) The Bought stage's money block was
      a flex strip of up to 8 equal columns (squashed narrow, sparse
      wide), now a proper receipt, one figure per line in a quiet
      bounded box, Car price and All in ruled off and larger.
      "Everything else" is no longer a .jstage at all: a new flat
      .dsec section (small caps heading "Other details", no dot, no
      chevron, no card border, unindented) so it reads as reference
      data under the journey, never a fifth stage; the drawer's
      DealerKit pane got the same flat treatment for the same reason.
      Gutters standardised on 20px throughout the drawer (header,
      stages, next up, cancel banner, timeline, comments). 600 tests
      pass, node --check clean, verified in the real browser at both
      widths against the real BMW X1 record.
    - FINE TOOTH COMB AUDIT AND POLISH PASS, same day (Steven: "there
      are things broken, and there are things missing from the build
      that we discussed... the usability is quite shocking... do a user
      experience test and find faults that are obvious and not so
      obvious"). Run as a real hands on UX test, not a code review: two
      throwaway manual cars (TEST111, TEST222) driven through the WHOLE
      real lifecycle in the browser, add by hand, drag resize, every
      editor type, video to signed off, person collecting, paid for, a
      chip, check in, and a full cancellation, every screenshot
      inspected, then both cars and every trace of them (events, chips,
      walked_away, the test saved view) deleted from the real database
      and verified gone. What the test PROVED working: the drag resize,
      the whole journey progression, the chip preview and recording,
      the cancel dialog, search, sorting, the Fields menu round trip,
      check in, comments landing in both places.
      BROKEN, found and fixed:
      (1) A manual car's platform pill was INVISIBLE, white text on
      nothing, .plat.manual had no CSS anywhere (both the purchases and
      lost bids pages share the rule, both fixed).
      (2) A cancelled car's record lost its whole bought history, the
      Car details pane showed nothing but Other details, the money and
      dates gone; it now keeps a flat Bought section (platform, date,
      mileage, the full money receipt) above Other details.
      (3) Every cancelled car's Status read "Due in", nonsense; the
      Status cell and the record's own Status row now say Cancelled in
      red on a cancelled car, and are not editable there.
      (4) The video gate followed "the last option in the list" rather
      than the option actually FLAGGED gate in settings, so reordering
      the list in the new editor would have silently moved the gate;
      videoGateValue() now reads the flag, last option only as a
      fallback, and the stage's own note names the real gate option.
      (5) The default flags on dropdown options did nothing at all: an
      untouched Video cell showed "–" when the car genuinely IS
      "Awaiting confirmation"; an empty cell now shows the default
      flagged option's pill faded, titled "The default, not set by
      hand yet".
      (6) Escape closed nothing; it now backs out one layer at a time,
      editor or menu, then dialog, then the drawer, leaving the photo
      gallery's own handler alone.
      (7) Timeline entries showed raw stored values ("1" for paid, a
      bare ISO datetime); paid_for reads Yes/No, dates and mileage read
      their human shape, display only.
      (8) Moving a car to In stock straight from the table's Status
      cell left the record's Arrived stage offering a Check in for a
      car already archived; it now says plainly "In stock, handled in
      DealerKit from here" with no fake arrival stamp.
      (9) Table dates were raw ISO (2026-08-26); Bought and Due in
      date now read 26/8/2026 like everywhere else on the record.
      MISSING FROM THE DISCUSSED BUILD, now built:
      (10) Column rollups at the foot of the table (Steven's explicit
      "rollups at the bottom of the columns"): a real tfoot with the
      car count, Car price total, Chipped total, Retail est. total and
      a paid count, built from the SAME column visibility calls as the
      header and rows so they can never disagree.
      (11) Saved views (Steven's original "...and saved as a view"): a
      Views menu next to Fields saves the page exactly as it stands
      (view tab, platform tab, search, dates, sort) under a name,
      apply with one click, delete with a confirm; stored server side
      per dealership (settings key purchase_views, its own validator
      in serve.py, only known criteria keys survive), so views hold on
      every device. 4 new tests.
      (12) Per view column tailoring, the not so obvious usability
      fault: the Cancelled view used to show every working list column
      (Video, Person collecting, Paid...) and pushed the cancellation
      reason, the one column that view EXISTS for, off the edge of the
      screen; In stock had the same problem milder. VIEW_HIDE now
      trims each view to the columns that mean something there, the
      Cancelled view reads Photo, Reg, Vehicle, Platform, Bought,
      Notes, Car price, Chipped, Retail est., Cancelled, all on screen
      with no sideways scroll, and the Fields menu's own choices still
      apply within that.
      Verified live end to end after every fix, 604 tests pass, node
      --check clean, no leftover template tokens, and the real page
      confirmed back to Steven's own 21 car working list with zero
      test data left behind.
    - THE DRAWER REBUILT AS RECORD LEFT, JOURNEY RIGHT, same day
      (Steven: "car details need to be its own section and the actual
      flow needs to be its own section too... you have fields in
      places that doesnt even make any sense", then, from three
      mockups, "i actually like c, but lets force it to always be
      wide so its always side by side. then always show car details
      on the left and move the tabs to the right so the 3 tabs are
      journey - timeline - dealerkit", then "in the dealerkit section
      it needs to have all the dealerkit sync options showing whats
      missing and whats not synced", then "that looks perfect").
      Designed as three interactive mockups on an artifact first
      (Record and Journey Concepts), built only after his sign off.
      The underlying rule that fixed the confusion: EVERY FIELD HAS
      EXACTLY ONE HOME. The record owns the facts, the journey owns
      only what moves the car forward, and a journey row may quote a
      fact but never edits it.
      The built layout: the drawer always opens wide enough for the
      split (940px default, drag clamped to at least 780, a
      remembered narrower width from the old drawer is clamped up on
      open), header (identity, stepper, money line) spanning both
      columns, then a permanent CAR DETAILS column on the left (four
      groups: Car, Money receipt, Collection and logistics, Notes and
      documents, pencil marks on every editable row) and a right
      panel carrying the three tabs Journey, Timeline, DealerKit. The
      old expanding stage cards, the "Other details" section and the
      Car details tab are gone entirely (markup, JS and CSS all
      removed, not disabled). The container query now handles only
      the genuinely narrow case, a phone, by stacking the right panel
      under the record.
      The Journey tab: the Next up card plus a vertical rail, one row
      per stage with ONLY its single closing control, the video
      status pill, the Paid for tick, the Check in button (disabled
      with "Finish the earlier stages first" until it is the current
      stage, a real [disabled] style added after the first live check
      showed it rendering full strength). Collection's row quotes the
      booking and collector from the record, read only.
      The DealerKit tab is the FULL sync panel, not a read only
      summary: every item (Car, Purchase price, Auction fee,
      Delivery, Indemnity, Photos, Documents, Service history, V5,
      Retail price, Due in stage, Due in date) in one of four honest
      states, a green tick with the real figure when on DealerKit, a
      red ring when missing there, a blue ticked box when BidBrain
      has it to push (the gap spelled out, "£7,500 on DK, £7,250
      here"), dim "Not charged on this purchase" when not applicable,
      grey rings before any check has run. A price mismatch after a
      chip arrives pre ticked with the warning underneath. Upload
      ("Upload N ticked items to DK", the count live from the boxes)
      and Check DK record both run from the panel through the same
      endpoints and progress polling the table row uses, and
      loadPurchases now repaints an open drawer, so a push or check
      finishing shows in the drawer too, not just the table behind
      it. The mileage hard block holds here identically, the button
      reads "Enter the mileage first" and disables outright.
      Verified live end to end on the real BMW X1: the split renders,
      the video pill opens its options from the rail, the DK panel
      showed the car's true never checked state with the correct
      "Upload 3 ticked items" count, the disabled Check in reads
      disabled, and Steven's own data was left untouched throughout.
      604 tests pass, node --check clean, no orphan handlers, no
      leftover template tokens.
    - COLUMN DRAG REORDER, COLUMN RESIZE, AND THE DROPDOWNS MADE
      HONEST, same day (Steven: "you still cant drag and drop the
      order of the columns? and you still cant resize a column? this
      was supposed to be in the build? but you missed it... also when
      you click a dropdown you cant click off it properly, it also
      isnt clear to the user that they are actual dropdowns"). He was
      right on all three, reorder and resize were part of the
      Airtable brief and had been sitting on the deferred list.
      (1) The whole table (rows, header, footer, and a new colgroup)
      was refactored from a hard coded column concatenation to ONE
      ordered list of column keys, so all four can never disagree.
      Each column's cfg (the same purchase_columns settings the
      Fields menu writes) gained optional pos (drag order) and w (a
      dragged width), validator widened in serve.py, saved per
      dealership like everything else on this page. Photo, Reg and
      Vehicle stay fixed first, the Cancelled column pinned last,
      everything between drags: real HTML5 drag and drop on the
      headers with a drop side indicator, reordering over the FULL
      movable list so hidden columns keep their relative places, the
      Fields menu now lists columns in the same configured order.
      (2) Resize: a grip on every header edge (Photo excepted),
      pointer drag sets the col element's width live and saves it on
      release (48 to 700px); a never resized column keeps the
      browser's own natural width so nothing jumped when this
      shipped. Honest limit: the table keeps auto layout (fixed
      layout plus overflow clipping would have clipped the DK
      rollover popovers), so dragging a column NARROWER than its own
      content stops at the content's minimum, widening always works.
      A grip press can never fire the sort, and a drag started on the
      grip never starts a column drag.
      (3) Dropdown dismissal was genuinely broken in three ways:
      clicking the same cell again blinked the menu shut and straight
      back open (now a real toggle, suppressReopen), several cells
      stop the click to protect their own action so the plain
      document click closer never ran (now a capture phase
      pointerdown closes menus on ANY press outside them, which
      stopPropagation on click cannot swallow), and scrolling left
      the position fixed menu hanging in space (any scroll now closes
      menus and commits an open text editor through its own blur).
      Opening Fields no longer leaves Views open either, closeMenus
      finally knows about all three panels. The state lives ABOVE the
      functions that use it, the third time this file nearly hit its
      own documented temporal dead zone trap.
      (4) Affordance: every dropdown cell (table, drawer record rows,
      the journey rail's video pill) now carries a small chevron
      caret, darkening on hover; the table's text and date cells show
      a pencil on hover, matching the drawer's own. And a sorted
      column can finally be UNSORTED, a third header click clears
      back to the view's own default order, there was previously no
      way back at all.
      Verified live: the same cell toggle proven with real clicks,
      reorder and resize proven by dispatching real DragEvents and
      PointerEvents through the actual handlers (this pane's
      synthetic mouse drags cannot trigger native HTML5 drag, the
      same tool limitation as the datetime segments), Car price
      genuinely moved before Bought and persisted to the settings
      table, the Vehicle column resized 250 to 310px and saved; every
      test artifact (positions, widths) was then stripped from the
      real settings with Steven's own column visibility choices
      verified intact. 604 tests pass.
    - RESIZE ACTUALLY FIXED, AND THE TABLE'S TITLES PINNED, same day
      (Steven, with a screenshot: "everything in this screenshot
      including the titles should be fixed and not affected by the
      scroll of the table. also the resizing the columns still doesnt
      work"). The resize failure had a real root cause the first
      verification MISSED: the native drag's SOURCE is the draggable
      th itself, so dragstart fires with the th as its target, never
      the grip, the grip check in dragstart could not match, and a
      real mouse press on the grip became a column drag that cancels
      the pointer stream dead. The synthetic pointer test had passed
      because it never exercised the native drag machinery at all, a
      false pass worth remembering: a synthetic event test proves the
      handler, never the browser's own competing gesture. Fixed belt
      and braces: the grip's pointerdown suspends the th's draggable
      for the length of the gesture (restored on pointerup AND
      pointercancel), captures the pointer so move and release always
      arrive, and dragstart additionally refuses while a resize is in
      flight.
      The titles: the table now scrolls INSIDE its own box (the
      wrapper capped to the viewport by sizeTableArea, measured, not
      a guessed calc, since the run bar and notices above come and
      go), so the view buttons and toolbar never move, the column
      titles are position sticky at the box's top and the rollup
      footer at its bottom, always visible. border-collapse had to
      become separate, collapsed borders shed from sticky headers in
      Chrome as they float.
      A REAL MISTAKE MADE AND PUT RIGHT during this: the test
      cleanup, stripping the widths and positions the verification
      pass itself had saved, assumed EVERYTHING it found was its own
      artifact, but Steven was using the feature live at that exact
      moment and most of what it found was HIS OWN first real column
      arrangement (Status first, then Bought, then Platform, his own
      widths), wiped along with the test values. The exact values had
      been printed before the wipe, so his whole layout was restored
      verbatim and verified equal; the one ambiguous value is the
      Video column width (210px, which may include 40px from a test
      drag on that same grip), one drag for him to correct if so.
      The lesson, recorded so it sticks: on a LIVE system the user
      may be acting between verification steps, so a cleanup must
      only ever remove values it can positively attribute to the
      test (captured before and after), never "whatever is there
      now". 604 tests pass.
    - RESIZE FIXED FOR REAL AT THE THIRD ATTEMPT, same day (Steven:
      "the resizing still isnt working, fix it properly!"). TWO more
      real causes found, each masked by the last:
      (1) The grip was nearly unhittable: 9px wide with 4px hanging
      into the NEXT header, and since every sticky header is its own
      stacking context the neighbour painted over the overhang,
      leaving about 5 invisible pixels. Reworked: a wide right grip
      fully inside its own header PLUS a left grip on every header
      that resizes the PREVIOUS column, so the whole strip around a
      border grabs from either side, with a divider that shows on
      header hover so it can be found at all.
      (2) The deepest one: even when the handler ran and saved, the
      table used auto layout with width 100%, so the browser treated
      the col widths as SUGGESTIONS and redistributed them, the saved
      number changed nothing on screen. Every earlier "pass" had
      checked the style attribute or the config, never the RENDERED
      width; Steven's own drags were genuinely saving figures (a 355px
      Vehicle width sat in his config from an attempt that visibly
      did nothing). Fixed with table-layout fixed, a width for every
      column (the dragged figure or a sane default), the used table
      width being the larger of 100% and the column sum so it still
      fills when narrow and scrolls sideways when wide, and overflow
      hidden on the text cells EXCEPT the popover hosts (Auc status,
      the money columns, DealerKit) whose rollovers would be clipped
      with them.
      Proven the right way at last: a real CDP mouse drag on the grip
      (the same input pipeline as a real mouse, not a synthetic
      PointerEvent) moved the RENDERED width 310 to 370 and saved it;
      config snapshotted before the test and restored verbatim after,
      the lesson from the wipe earlier the same day applied. The
      standing verification lesson now twice earned: check the
      outcome the user sees (rendered layout), never the mechanism's
      own bookkeeping. 604 tests pass.
    - THE CELL EDITORS MADE INLINE, THE DATE PICKER OPENS ITSELF, same
      day (Steven: "the collection arranged date picker doesnt work as
      expected, it should just be the date and time picker, no typing,
      and it should be inline, its behaving funny. also the same can
      be said for notes"). The old editor was a floating box appended
      to the body and positioned NEAR the clicked cell: the datetime
      one landed as a segmented typing field whose tiny calendar icon
      needed a second, precise click, the notes one was a floating
      textarea where Enter made a newline instead of saving, and both
      drifted or dismissed oddly around the new scroll and pointer
      handling. Rewritten to edit IN PLACE: the input replaces the
      clicked cell's own content (a record row edits in its value slot
      so the label stays put), a date field calls the browser's own
      showPicker() immediately so the real calendar opens on the first
      click with no typing, notes save on Enter (Shift Enter for a new
      line), Escape backs out without saving and without falling
      through to close the drawer, and clicking anywhere else saves
      through blur. closeFloaters now BLURS an open inline editor
      rather than removing it, so no dismissal path can ever throw an
      edit away, and the fixed table layout means the inline input
      fits its cell exactly. Verified live on a throwaway manual car
      (TEST333, deleted with its events afterwards, zero traces): the
      datetime editor rendered inside its cell focused with the native
      picker up (the popup held focus against synthetic keys, itself
      the proof it opened), a committed value saved and rendered as
      29/8/2026, 2:30pm, real typed notes committed on Enter, and
      Escape discarded a poisoned value without touching the saved
      one. One tool note for future sessions: this pane's key action
      does not reliably deliver Enter or Escape to the page (typing
      works), so keyboard paths are verified with dispatched
      KeyboardEvents instead. 604 tests pass.
    - LONG NOTES NO LONGER KICK YOU OUT, PENCE ON EVERY PRICE, UK
      DATES, same day (Steven: "notes is broke, you can only type so
      many characters and then it kicks you out and wont let you edit
      them... prices across the app should have two decimal points for
      pence... dates are in american format, this is a uk app").
      (1) The notes kick out had a precise mechanism: the capture
      phase scroll listener (added earlier the same day to dismiss
      floating menus on scroll) caught the textarea's OWN internal
      scroll, which fires the moment the text wraps past the visible
      box, and blurred the editor mid word; opening an already long
      note scrolled to the caret instantly, so editing was killed on
      arrival, exactly "wont let you edit them". The listener now
      ignores scrolls originating inside an inline editor, the notes
      box starts two rows tall and GROWS with the text (capped at
      220px, then scrolling inside itself, safely now), and the cell
      being edited lifts its own overflow clipping for the duration so
      the grown editor is never cut off. Proven live with a 217
      character note typed in one go, editor open and focused
      throughout, saved intact.
      (2) Money: pounds() on both the purchases and Didn't win pages
      now always shows pence (en-GB, exactly two decimals), and every
      raw toLocaleString money spot (the chip cell, the chip receipt
      lines, the footer chip total, the header's "after £X chipped")
      went through a shared poundsNeg/pounds instead, so £4,820.8
      style one decimal figures are gone. The cockpit's own valuation
      figures (max bid, retail) deliberately stay whole pounds, they
      are estimates that never carry pence; say the word if those
      should change too.
      (3) Dates: fmtDT and fmtStamp now zero pad (27/08/2026), the
      drawer's money line no longer shows a raw ISO bought date, and
      the Didn't win page's own date column (raw ISO, year first, the
      likeliest "american format" sighting) now reads DD/MM/YYYY via
      its own ukDate helper, which a placement check caught being
      defined on the WRONG page's script first (each page carries its
      own script, a helper on one is undefined on the other), fixed
      before it shipped. One honest caveat told to Steven: the date
      picker's own segment order inside the native input follows
      Chrome's language setting, not the app; every date the APP
      renders is UK. 604 tests pass, both pages' generated JS node
      checked, the throwaway test car deleted with zero traces.
    - PENCE ONLY WHEN REAL, AND THE NOTES CELL BACK IN LINE, same day
      (Steven: "pence should never show if it is .00... the notes are
      working better but they are now out of line with the rest of the
      table"). pounds() on both money pages now shows pence only when
      they are non zero: £7,500 stays clean, £4,820.80 keeps both
      digits, proven against the page's own live function (£7,500 /
      £4,820.80 / £1,234.56 / £0.50 / –). The misalignment had a
      precise cause: the .notescell CSS dated from the OLD page where
      it styled an inline SPAN, and its display:inline-block, applied
      since the rebuild to the td itself, stopped that cell being a
      table cell at all and knocked the whole row out of column.
      Trimmed to ellipsis and font size only; verified live that the
      notes td computes as table-cell again and every one of the 18
      cells in a real row sits within 2px of its own header's left
      edge, zero misaligned. 604 tests pass.
    - A TICK TO FINISH A NOTE, AN HONEST EMPTY SERVICE HISTORY, AND
      DOCUMENT PHOTOS ON THE RECORD, same day (Steven: "when you start
      writing a note there needs to be a tick button at the end...
      currently its awkward to click out of it. if a car has no
      service history, instead of being blank it needs to say No
      Service History. also in the car overlay you should be able to
      see the uploaded service history pics as well as the v5 pics
      the same way you can in the table view").
      (1) The notes editor grows a green tick button under its right
      corner; its pointerdown is prevented so pressing it never blurs
      the textarea first, the click is the one clean commit path
      (Enter and clicking away still save, Escape still discards).
      (2) _docCell gained an empty state parameter: the Service
      history cell reads a small red "No service history" instead of
      a dash, in the table AND on the record, never a blank.
      (3) The record's Notes and documents group shows the REAL
      uploaded service history and V5 photos as clickable thumbnails
      through the exact same _docCell and shared gallery the table
      row uses (mediaUrl for Motorway's urls, localDocUrl for
      Carwow's local files), replacing the old bare page counts.
      Verified live end to end: a throwaway car proved the tick
      commits ("Saved with the tick button" persisted, editor
      closed), the empty state shows in both places, and a real
      documented car's record row opened the shared gallery from its
      own thumbnail; the test car deleted with zero traces. 604
      tests pass.
    - STILL TO DO, deliberately deferred: a fee band editor UI,
      CompMatch deliberately keeps using the pre chip bid as its comp
      (a chip reflects condition found, not market level).
      STANDING REMINDERS: Steven's own NEXT JOB after this is the
      DealerKit half of Check in (pushing the arrival into DealerKit);
      dealerkit_write.set_purchase_price has never been watched live,
      its first real use must be watched not trusted; the platform
      cancellation proof pack is a future feature. FOR MARK when this
      lands: he must repeat the data tidy up on his own database (his
      own cancelled/archived split, his own bid recovery IF his
      account's arithmetic checks out, the Motorway Move caveat in the
      memory file means a blind rerun of Steven's recovery could be
      wrong AND pass its own check), and he should be asked his own
      dropdown answers (all columns), whether he wants Location and
      the Due in date column, what to do with Auc Status, and told
      video_stage defaults off for him; A4C, Dealer Auction and
      DealerWay purchases remain his own later build.

