ó
    “íÃjO� ã            
       óÞ  • S r SSKrSSKrSSKrSSKrSSKrSSKrSSKrSSK	r	SSK
r
SSKrSSKrSSKrSSKJrJrJrJrJr  SSKJr  SSKJrJrJr  SSKJrJrJr  SSKrSSKJ r   \!" \ V s1 s H  o S   iM	     sn \ V s1 s H  o S   iM	     sn -  5      r"\RF                  RI                  \RF                  RK                  \&5      5      r'S	r(\RF                  RS                  \'S
S5      r*\RF                  RS                  \'S
S5      r+\RF                  RS                  \'S
SS5      r,\RZ                  " 5       r.0 r/Sq01 Skr1\1S1-  r2S r3S r4S r5S r6S r7\Rp                  r9Sr:Sr;\<" \Rz                  5      \<" \R|                  5      -    Vs0 s H  o\R                  U5      _M     snr@SrAS rBS rCS�S jrDS rES rFSrGS0 0 4S .rHS! rIS" rJS# rKSŽS$ jrLS% rMS�S& jrNS' rOS(S)S*S+S,S-.rPS.S/1rQS0 rRS1 rS\R¨                  " S25      rUS3 rV\R¨                  " S45      rWS5rXS6 rYS7 rZ1 S8kr[S9 r\S: r]S; r^S< r_S= r`S> raS? rbS@ rcSA rdSB reSC rfSD rgSEqhSqiSqjSqkSqlSEqmSqn\	R                  " 5       roSrpSF rq\r" 1 SGk5      rsSHSISJSKSLSMSNSO.rtS�SP jruSQ rvSR rwS�SS jrxST rySUSVSWSXSYSZS[S\S]S^.	rzS_ r{S‘S` jr|Sa r}/ r~Sb rSc r€Sd r�Se r‚Sf rƒSg r„Sh r…S’Si jr†S“Sj jr‡Sk rˆSl r‰SmrŠSnr‹S�So jrŒS”Sp jr�Sq rŽSr r�Ss r�S/ St.r‘Su r’Sv r“Sq”Sw r•Sx r–Sy r—Sz r˜S{r™S| ršS•S} jr›S~ rœS r�S€ ržS� rŸSSS‚.r Sƒ r¡S�S„ jr¢S… r£S† r¤S‡ r¥Sˆ r¦ " S‰ SŠ\GRN                  GRP                  5      r©S‹ rª\«SŒ:X  a  \ª" 5         ggs  sn f s  sn f s  snf )–a  
The local server for the cockpit page. Serves this project folder and, unlike a
plain file, accepts the page's save actions so Steven's hide and bid choices
persist to the local database on the Mac Studio.

It reads screens and serves a page only. It never places a bid. The bid list is
Steven's own shortlist.

Run with:  python3 serve.py
Then open: http://localhost:8765/cockpit.html

Important: the page must be opened at this address (or the private link), never
as a file, or the hide and bid buttons cannot save.
é    N)ÚdbÚrenderÚbid_suggestÚ
comp_shareÚdealerkit_write)Úruns)ÚDEFAULT_SETTINGSÚBANNED_ENGINESÚ
LITRE_BANS)ÚPLATFORM_KEYSÚINTEGRATION_DEFAULTSÚPLATFORM_ENABLED_DEFAULTS)Údealer_retail_priceÚnamei="  Údatazlast_run.jsonzrun_progress.jsonÚlogszmanual_run.log>
   ÚglassÚcarwowÚcazanaÚmotorwayÚ	dealerkitÚ	dealerwayÚ	lecapitalÚauction4carsÚdealerauctionÚmotorway_accountsÚ	dealer_osc                  ó    • [         R                  " 5       [        R                  " [	        [
        SS5      S9-   n [        R                  " U 5      $ )aB  The same real comp pool a live shortlisted car's own gauge uses
(this Mac's own history plus any other dealership's shared comps),
built fresh on every call, never cached, so a fresh day's comps are
reflected immediately. Shared by _purchases_with_bias and
_lost_bids_with_bias so both never build it twice in one request.ÚDEALER_NAMEÚ )Úexclude_dealer)r   Úbid_comps_fullr   Úload_shared_compsÚgetattrÚdealer_configr   Ú
build_pool)Ú	all_compss    Ú&/Users/stevendouglas/BidBrain/serve.pyÚ
_comp_poolr)   J   sC   € ô ×!Ò!Ó#¤j×&BÒ&BÜœ}¨m¸RÓ@ñ'Bñ B€Iä×!Ò! )Ó,Ð,ó    c                 ó  • U R                  S5      =(       d    SR                  SS5      n[        R                  " U(       a  US   OS[	        U5      S:”  a  US   OSU R                  S5      U R                  S5      U R                  S5      U R                  S	5      U R                  S
5      S9nSU ;   a!  U R                  S5      (       a  U S   S-  OSnOU R                  S5      n[
        R                  " X1US9nU(       a  US   $ S$ )a¯  What CompMatch would suggest today for one purchase or lost bid
row (Mark 2026-08-25: "add the ai suggested bid (Bias)..."). Builds
make/model from the row's own free text name (the same split()
pattern purchases_run.py's own Cazana lookup already uses) and reads
mileage/year/transmission/engine/grade straight off the row, all
real columns on both tables since the CompMatch work earlier the
same day. car_retail is the row's own plain, unscaled Cazana retail
(purchases: cazana_115 / 1.15; lost bids: cazana_retail directly,
already unscaled), matching how every other CompMatch call site
derives it. None, never guessed, when there is not enough real
comparable history for this car.r   r    Né   r   ÚmileageÚyearÚtransmissionÚengineÚgrade)ÚmakeÚmodelr-   r.   r/   r0   r1   Ú
cazana_115çffffffò?Úcazana_retail)Ú
car_retailÚ	suggested)ÚgetÚsplitÚtypesÚSimpleNamespaceÚlenr   Úsuggest_bid_for_car)ÚrÚpoolÚpartsÚcarr7   Úsuggs         r(   Ú_bias_bid_for_rowrD   U   sà   € ð �U‰U�6‹]× ˜b×'Ñ'¨¨aÓ0€EÜ
×
Ò
ÞˆU�1ŠX B¼#¸e»*Àq».¨e°AªhÈbØ—‘�iÓ  q§u¡u¨V£}À1Ç5Á5ÈÓCXØ�u‰u�X‹ a§e¡e¨G£nñ6€Cð �qÓØ12·±°|×1DÑ1D�a˜‘o¨Ò,È$‰
à—U‘U˜?Ó+ˆ
Ü×*Ò*¨3ÀÑL€DÞ $ˆ4�ÑÐ.¨$Ð.r*   c                  óž   • [         R                  " 5       n [        5       nU  H  n[        X!5      US'   M     U [         R                  " 5       -   $ )aò  Every purchase, each carrying its own bias_bid, see _bias_bid_for_row.

Plus the cars whose sale fell through that never made it into the
purchases table at all (db.cancelled_shell_rows). They are the same real
thing as a cancelled purchase, so the Cancelled view has to show them or
it lies: it was showing 88 of 541 (Steven 2026-08-30). They join HERE, in
the page's own feed, deliberately not in db.list_purchases(), which also
feeds the DealerKit push, the accounts email and the platform sync.Úbias_bid)r   Úlist_purchasesr)   rD   Úcancelled_shell_rows)Úrowsr@   r?   s      r(   Ú_purchases_with_biasrJ   n   sE   € ô ×ÒÓ€DÜ‹<€DÛˆÜ)¨!Ó2ˆˆ*‹ñ à”"×)Ò)Ó+Ñ+Ð+r*   c                  óv  • [         R                  " 5       n [        5       n[         R                  " 5       nUR	                  SS5      nUR	                  SS5      nU  HZ  n[        XQ5      US'   [        UR	                  S5      X4S9US'   UR	                  S5      (       a  [        US   S	-  S
5      OSUS'   M\     U $ )a|  Every lost bid, each carrying its own bias_bid too (Mark
2026-08-25: "on the lost bids page add the % of bias to winning
bid"), the same real CompMatch suggestion a purchase's own Winning
bid popover already shows, compared here against sold_for (the real
price it actually took someone else to win, a lost bid's own
equivalent of a purchase's winning_bid). Also a retail_estimate (Mark:
"also add retail est to the columns"), the same dealer_retail_price()
formula and dealer price endings the purchases page's own Retail est.
column already uses, computed fresh here since lost_bids only ever
stores the plain cazana_retail it was built from, not this rounded,
dealer-facing figure; and cazana_115 (Mark: "also add the cazana 115%
value column"), the same plain retail-times-1.15 uplift the purchases
table already stores directly as its own column, computed fresh here
for the same reason.Úretail_estimate_marginéô  Úretail_estimate_endingé_   rF   r6   )ÚmarginÚendingÚretail_estimater5   é   Nr4   )r   Úlist_lost_bidsr)   Úget_settingsr9   rD   r   Úround)rI   r@   ÚsavedrP   rQ   r?   s         r(   Ú_lost_bids_with_biasrX   ~   s¯   € ô ×ÒÓ€DÜ‹<€Dô
 �OŠOÓ€EØ�Y‰YÐ/°Ó5€FØ�Y‰YÐ/°Ó4€FÛˆÜ)¨!Ó2ˆˆ*‰Ü2°1·5±5¸Ó3IÐRXÑhˆÐ
ÑØABÇÁÀ×AWÑAWœ%  /Ñ 2°TÑ 9¸1Ô=Ð]aˆˆ,‹ñ ð €Kr*   c                  ób  ^• SSK Jn JnJnJn   U " 5       u  pEpgU(       d  U(       a  U" XE5      O/ nU" 5       n	U	(       a  X‰/-   nXƒ" 5       -   nU(       a  [        R                  " U5      O	[        5       m[        U4S jU 5       5      n
XŠ4$ ! [
        [        R                  4 a    / / Spun N”f = f)aº  The same health notices daily_run._write_pages already computes for
the cockpit's own bell, plus the same real shortlisted count its own
nav star shows, both recomputed here from the last saved run cache so
the new standardised bell and star on purchases/lost-bids/settings show
the identical picture (Mark 2026-08-25: "standardise the nav to icons
for favorites, purchases, notifications, lost bids and settings", then
"star go yellow when watched / shortlisted cars"). Nothing here ever
logs into anything on its own, same as the cockpit's own notices, this
only reports what a real run already found. A missing or empty cache
(never run yet) is a quiet empty list and a zero count, not an error.r   )Ú_load_cacheÚ_session_health_noticesÚ_stock_health_noticeÚ_platform_health_noticesNc           
   3   ó¸   >#   • U  HO  n[         R                  " TUR                  R                  [	        UR                  S S5      5      (       d  MK  Sv •  MQ     g7f)ÚsourceNr,   )r   ÚstarredrB   Úregr$   )Ú.0ÚaÚbidsets     €r(   Ú	<genexpr>Ú_notices.<locals>.<genexpr>³   s<   øé € Ðl¢˜A¬b¯jªj¸ÀÇÁÇÁÌGÐTU×TYÑTYÐ[cÐeiÓLj×.k—a‘a¢ùs   ƒA
AÁ	A)Ú	daily_runrZ   r[   r\   r]   ÚFileNotFoundErrorÚjsonÚJSONDecodeErrorr   Úbids_forÚsetÚsum)rZ   r[   r\   r]   Ú	shortlistÚheldÚrejectedÚ	sale_dateÚnoticesÚ
stock_noteÚshortlistedrd   s              @r(   Ú_noticesru   �   s«   ø€ ÷ oÓnð2Ù/:«}Ñ,ˆ	˜ö <EÎÑ% iÔ6ÐSU€GÙ%Ó'€JÞØ˜LÑ(ˆØÐ0Ó2Ñ2€GÞ'0ŒR�[Š[˜Ô#´c³e€FÜÔl¡ÓlÓl€KØÐÐøô œt×3Ñ3Ð4ó 2Ø%'¨¨T˜ˆ	™ð2ús   �
B ÂB.Â-B.éZ   éX  zStopped from the page.c                 ól  •  [         R                   R                  U 5      n[         R                   R	                  U5      U-
  nUR                  5       nUS:  a  gUS:  a  [        US-  5       S3$ US:  a  [        US-  5       S3$ US:  a  g	[        US-  5       S
3$ ! [        [        4 a     gf = f)z¿"20h ago" for a db._now() timestamp (a local naive ISO string),
measured against the server clock the caller already has. Empty string
on anything unparsable, so a light never invents an age.r    é<   zjust nowé  zm agoi€Q zh agoi £ Ú	yesterdayz	 days ago)ÚdatetimeÚfromisoformatÚ	TypeErrorÚ
ValueErrorÚfromtimestampÚtotal_secondsÚint)ÚatÚnowÚthenÚsecss       r(   Ú
_age_wordsr‡   Í   sÇ   € ðÜ× Ñ ×.Ñ.¨rÓ2ˆô ×Ñ×*Ñ*¨3Ó/°$Ñ6€DØ×ÑÓ€DØˆbƒyØØˆdƒ{Ü�d˜b‘j“/Ð" %Ð(Ð(Øˆeƒ|Ü�d˜d‘lÓ#Ð$ EÐ*Ð*Øˆfƒ}ØÜ�$˜%‘-Ó Ð! Ð+Ð+øô ”zÐ"ó Ùðús   ‚B  Â B3Â2B3c                 ó(  • [        UR                  S5      =(       d    / 5      nU [        ;   a6  UR                  S5      =(       d    0 R                  U 5      SL =(       d    X;   $ [        R                  R                  U 5      nUc  gUR
                  (       aQ  UR                  S5      =(       d    0 R                  UR
                  5      =(       d    0 nUR                  S5      SL a  g[        UR                  5      =(       a    UR                  U;   $ )zâWhether this run_status key belongs to something switched off or
hidden in Settings, so its last failure must read as OFF rather than
BROKEN. Platforms read their own toggle; everything else reads the
registry's gate and site.Úhidden_connectionsÚplatform_enabledFÚintegrationsÚenabledT)rl   r9   r   Ú_runsÚRUNSÚgateÚboolÚsite)ÚkeyÚctxÚhiddenÚrunÚintegs        r(   Ú_run_key_offr—   â   sÊ   € ô
 �—‘Ð-Ó.×4°"Ó5€FØ
Ô'Ó'Ø—‘Ð*Ó+×1¨r×6Ñ6°sÓ;¸uÐD×UÈÉÐUÜ
�*‰*�.‰.˜Ó
€CØ
�{ØØ
‡x‡xØ—‘˜Ó(×.¨B×3Ñ3°C·H±HÓ=×CÀˆØ�9‰9�YÓ 5Ò(ØÜ�—‘‹>×0˜cŸh™h¨&Ñ0Ð0r*   c                 óæ  • [        U[        5      (       a  UOSnU=(       d    0 R                  S5      =(       d    Sn[        U=(       d    0 R                  S5      5      nSnU(       a1  UR                  S5      (       a  [	        SU=(       d    SUS   -
  5      nU=(       d    0 R                  S5      =(       d    SR                  5       n	U R                  S	5      (       a.  U R                  S
5      n
U=(       d    U
(       a  SU
 S3OSnSUS.$ [        S U R                  5        5       5      nU(       aˆ  U[        ;   =(       d    US:H  =(       d    US:H  =(       a    UnU(       aI  Ub/  U[        :¼  a%  SU	=(       d    U=(       d    S S[        U5       3S.$ SU	=(       d    U=(       d    SS.$ SU=(       d    SS.$ US:X  aM  U(       aF  Ub
  U[        :  a  SU	=(       d    SS.$ U[        :  a  SU	=(       d    S S[        U5       3S.$ SSS.$ U[        ;   aF  Ub
  U[        :  a  SU	=(       d    SS.$ U[        :  a  SU	=(       d    S S[        U5       3S.$ SSS.$ SnU=(       d    0 R                  5        Ho  u  nn[        U[        5      (       a  UR                  S5      SLa  M1  [        Xô5      (       a  MC  UR                  S5      =(       d    SnUb  UUS   :”  d  Mk  UU4nMq     U(       ax  Uu  nn[        UU5      n[        R                  Xÿ5      nUR                  U5      =(       d    0 R                  S 5      [        :X  a  S!OSnSU S"U 3U(       a	  S#U 3-   S.$ S-   S.$ SSS.$ )$a%  The one traffic light every page's nav bar paints (2026-08-31,
Steven: "no matter where you are or what page you are on you can see a
sync is happening and an animation showing its still running with a
traffic light system for easy to understand quick glances").

Deliberately PURE: data in, dict out, never a database path. db.py's
own functions default their path argument at definition time, so a
helper that reads for itself cannot be tested against a temp database
without silently hitting the real one. That trap is on record in
CLAUDE.md and has caught this project three times.

state is one of login, active, stalled, failed, ok. Order matters: a
login window is the most specific thing that can be true, a live proc
beats the file, and a past failure only shows once nothing is actually
running.NÚphaser    ÚpartialÚtsç        r   ÚmessageÚlogin_runningÚ
login_siteúA login window is open for Ú.úA login window is open.Úlogin©ÚstateÚlabelc              3   óf   #   • U  H'  u  pUR                  S 5      (       d  US:X  d  M#  Uv •  M)     g7f)Ú_runningÚrunningN)Úendswith©rb   ÚkÚvs      r(   re   Ú_sync_light.<locals>.<genexpr>  s.   é € ð ?¢™˜Ø—Z‘Z 
×+Ñ+¨q°I©~÷ ‘!¢ùs   ‚"1¨	1ÚstartingÚdoneÚstalledÚRunningu    Â· no update for Úactivez
Running...z!A scheduled run is in progress...zA scheduled runÚfailedzA run stopped without finishingÚokFrƒ   r,   ÚsummaryÚstoppedÚ u    Â· )Ú
isinstanceÚdictr9   r�   ÚmaxÚstripÚanyÚitemsÚ_ACTIVE_PHASESÚ_STALE_AFTERÚ_minsÚ_DEAD_AFTERr—   r‡   Ú_RUN_KEY_LABELSÚ_STOP_SUMMARY)ÚflagsÚprogressr„   Ú
run_statusr“   Úbusy_reasonr™   rš   Úager�   r‘   r¦   r©   ÚownÚworstr’   Úrowrƒ   Úwhenr   Úwords                        r(   Ú_sync_lightrÏ   ô   sm  € ô  & h´×5Ñ5‰x¸4€HØ�^˜× Ñ  Ó)×/¨R€EÜ�H—N ×'Ñ'¨	Ó2Ó3€GØ
€CÞ�H—L‘L ×&Ñ&Ü�#˜Ÿ˜q H¨T¡NÑ2Ó3ˆØ—˜B×#Ñ# IÓ.×4°"×;Ñ;Ó=€Gð ‡y�y�×!Ñ!Ø�y‰y˜Ó&ˆØ÷ @ÎÐ"=¸d¸VÀ1Ñ EØ%>ð 	à ¨5Ñ1Ð1ô ñ ? §¡¤ó ?ó ?€GæØœÑ&÷ 0¨%°:Ñ*=÷ 0Ø˜F‘?×. wð 	æØ‰ 3¬,Ó#6Ø!*Ø$+×$G¨{×$G¸iÐ#HÐHZÔ[`ÐadÓ[eÐZfÐ!gñið ià%°×0V¸;×0VÈ,ÑWÐWØ!¨K×,G¸<ÑHÐHð �ƒž7Ø‰;˜#¤Ó,Ø%°×0^Ð;^Ñ_Ð_Ø”ÓØ&Ø '× <Ð+<Ð=Ð=OÔPUÐVYÓPZÈ|Ð\ñ^ð ^à!Ð,MÑNÐNð& ”ÓØ‰;˜#¤Ó,Ø%°×0^Ð;^Ñ_Ð_Ø”ÓØ&Ø '× <Ð+<Ð=Ð=OÔPUÐVYÓPZÈ|Ð\ñ^ð ^à!Ð,MÑNÐNð €EØ×% 2×,Ñ,Ö.‰ˆˆSÜ˜#œt×$Ñ$¨¯©°«¸UÒ(BÙÜ˜×!Ñ!ÙØ�W‰W�T‹]× ˜bˆØ‰=˜B  q¡�MØ˜"�IŠEñ /ö Ø‰ˆˆRÜ˜"˜cÓ"ˆÜ×"Ñ" 3Ó,ˆð (Ÿ^™^¨CÓ0×6°B×;Ñ;¸IÓFÌ-ÓW‰yÐ]eˆØ!¨t¨f°A°d°VÐ,<ÖQUÀ$ÀtÀfÀÑ,^Ñ_Ð_Ð[]Ñ,^Ñ_Ð_ð  BÑ'Ð'r*   c                 óZ   • [        U 5      n U S:  a  U  S3$ U S:  a  U S-   S3$ U S-   S3$ )z½"45s" / "2m" / "3h", for a no-update age. Whole units only; this
sits in a tooltip, not a stopwatch. The hours tier matters: a stale
file at pass start used to render "no update for 1214m".ry   Úsrz   ÚmÚh)r‚   )r†   s    r(   rÁ   rÁ   j  sI   € ô ˆt‹9€DØˆbƒyØ��qˆzÐØˆdƒ{Ø˜"‘*�˜QÐÐØ�d‰lˆ^˜1ÐÐr*   c           
      óÚ   •  [        [        SSS9 n[        R                  " SU SSS[        R                  " 5       S.U5        SSS5        g! , (       d  f       g= f! [
         a     gf = f)	a^  Stamp the shared progress file "starting" the moment a pass is
spawned, so the light never dresses the PREVIOUS pass's leftover
message and age up as the new one's (the stalled-lie found in the
2026-09-01 sweep: clicking Sync now went amber with yesterday's
"Updated. 216 shortlisted..." and "no update for 1214m"). "starting"
is deliberately NOT in _ACTIVE_PHASES: while the proc flag is alive
rule 2 shows it, and if the pass exits before its first own write the
orphaned stamp is invisible to the launchd branch rather than walking
active -> stalled -> failed. Best effort, same as every progress
write.Úwúutf-8©Úencodingr¯   r   F)r™   r�   r°   Útotalrš   r›   N)ÚopenÚPROGRESSri   ÚdumpÚtimeÚ	Exception)r�   Úfs     r(   Ú_prime_progressrà   v  s]   € ðÜ”(˜C¨'Ò2°aÜ�IŠI 
°wÈØ !¨e¼4¿9º9»;ñHØIJôL÷ 3×2Ö2ûô ó Ùðús.   ‚A ‘2AÁA Á
AÁA ÁA Á
A*Á)A*g      @rœ   )rƒ   Úsnapc                 óÈ  • U [         S   -
  [        :  a  [         S   (       a	  [         S   $  [        R                  " 5       n[        R                  " 5       n0 [
        EUR                  S5      =(       d    0 E0 [        EUR                  S5      =(       d    0 EUR                  S5      =(       d    / S.nX4[         S'   U [         S'   X4$ ! [         a    [         S   s $ f = f)a  The settings half of the light, at most once every _LIGHT_TTL. The
pair is kept as ONE tuple written in one assignment: this server is
threaded, and two separate dict slots meant a concurrent poll could
read a fresh run_status against a stale ctx mid-update.rƒ   rá   rŠ   r‹   r‰   )rŠ   r‹   r‰   )	Ú_light_memoÚ
_LIGHT_TTLr   Úget_run_statusrU   r   r9   r   rÞ   )r„   rÇ   rW   r“   s       r(   Ú_light_inputsræ   •  sá   € ð
 Œ[˜ÑÑ¤Ó+´¸D×0AÜ˜6Ñ"Ð"ð#Ü×&Ò&Ó(ˆ
Ü—’Ó!ˆð!JÔ#<ð !JØ$)§I¡IÐ.@Ó$A×$GÀRð!JàYÔ3ÐY¸¿	¹	À.Ó8Q×8WÐUWÐYØ"'§)¡)Ð,@Ó"A×"GÀRñ	
ˆð &Ð+„K�ÑØ„K�ÑØˆ?Ðøô ó #ä˜6Ñ"Ò"ð#ús   ­BC ÃC!Ã C!c                  ó   • S[         S'   g )Nrœ   rƒ   )rã   © r*   r(   Ú_light_memo_clearré   ­  s   € Ø„K�Òr*   c            	      óˆ  • [        S5      n [        R                   Vs0 s H  o S3[        U5      _M     nnUS   US'   U (       a  [        OSUS'   Sn [	        [
        R                  R                  [        5      5      nSn [        [        SS9 n[        R                  " U5      nSSS5        [        R                  " 5       n [!        U5      u  px[#        X$XgU[%        5       5      n	0 UEX4X–S.E$ s  snf ! [         a     Nƒf = f! , (       d  f       Ne= f! [        [        4 a     Nzf = f! [&         a    S	S
S.n	 NVf = f)z½What is running right now, the live progress file, and the light.
One <key>_running flag per registry entry (login included) plus the
cockpit's historical plain "running" for the daily run.r£   r¨   Úrun_runningr©   NrŸ   rÖ   r×   rµ   r    r¤   )Úlast_runrÆ   Úlightr„   )r¨   r�   rŽ   Ú_login_siter‚   ÚosÚpathÚgetmtimeÚLAST_RUNÚOSErrorrÚ   rÛ   ri   Úloadr   rÝ   ræ   rÏ   Ú_busy_reasonrÞ   )
rž   r¬   rÅ   ÚlastrÆ   rß   r„   rÇ   r“   rí   s
             r(   Ú_run_statusr÷   ±  s6  € ô ˜WÓ%€MÜ27·*²*Ó=²*¨Qˆs�(ˆ^œX a›[Ò(±*€EÐ=Ø˜]Ñ+€Eˆ)ÑÞ)6�+¸D€Eˆ,ÑØ€DðÜ”2—7‘7×#Ñ#¤HÓ-Ó.ˆð €HðÜ”( WÒ-°Ü—y’y “|ˆH÷ .ô �)Š)‹+€Cð-Ü'¨Ó,‰ˆ
Ü˜E¨S¸cÄ<Ã>ÓRˆð YˆeÐX ÀeÒXÐXùò/ >øô ó Ùðú÷ .Õ-ûä”ZÐ ó Ùðûô ó -ð ¨Ñ,Šð	-úsY   žC3Á,C8 ÂD ÂDÂ)D Ã#D/ Ã8
DÄDÄ
DÄD ÄD ÄD,Ä+D,Ä/EÅ Ec                 ó’   • U =(       d    SR                  5       R                  5       nU(       a  US   R                  5       OSnUSU $ )z>One readable sentence from what may be a multi-line traceback.r    r   N)r¼   Ú
splitlines)ÚtextÚcapÚlinesÚfirsts       r(   Ú_first_linerþ   Ð  s=   € à�Z�R×ÑÓ ×+Ñ+Ó-€EÞ %ˆE�!‰H�N‰NÔ¨2€EØ��#ˆ;Ðr*   c                  ó‚  • [        5       n 0 n[        R                  " 5       =(       d    0 R                  5        H'  u  p#0 UES[	        UR                  S5      5      0EX'   M)     0 n[        R                  " 5       =(       d    0 R                  5        H>  u  pS[        U[        5      (       a   0 UES[	        UR                  S5      5      0EnX4U'   M@     UUU R                  S5      =(       d    0 U R                  S5      =(       d    / U R                  S5      =(       d    0 [        5       [        R                  " 5       S.$ )a  Everything the Run modal's own status board needs, fetched once when
it opens (2026-08-31: "theres currently no way of seeing from the live
auctions the current statuses of the last time the runs were made").

Deliberately NOT folded into _run_status above, which the cockpit polls
every three seconds while a run is going and which has to stay small.
This one is read on demand only.

Both status sources are sent, because they answer different questions
and can honestly disagree: run_status is when something last really
RAN (so the age of the cars on the list), site_health is whether the
CONNECTION works (and is also written by the nightly auto logins, so
it can read fresh on a day nothing ran). The modal shows the first as
the age and reaches for the second only to explain a failure.Údetailr�   r¶   rŠ   r‰   r‹   )rÇ   Úsite_healthrŠ   r‰   r‹   rÈ   r„   )Ú_merged_settingsr   Úget_site_healthr¾   rþ   r9   rå   r¹   rº   rõ   rÝ   )ÚsettingsÚhealthr‘   rÌ   rÇ   r’   s         r(   Ú_run_summaryr  ×  s	  € ô  Ó!€Hð €FÜ×(Ò(Ó*×0¨b×7Ñ7Ö9‰	ˆØI˜#ÐI˜x¬°S·W±W¸YÓ5GÓ)HÑIˆ‹ñ :à€JÜ×&Ò&Ó(×.¨B×5Ñ5Ö7‰ˆÜ�cœ4× Ñ ØE�SÐE˜)¤[°·±¸Ó1CÓ%DÑEˆCØ�3‹ñ 8ð
 !Øà$ŸL™LÐ);Ó<×BÀØ&Ÿl™lÐ+?Ó@×FÀBØ Ÿ™ ^Ó4×:¸ô $“~Ü�yŠy‹{ñð r*   c                 óÌ   •  [        U 5      nU(       a  US::  a  [        U S35      eU[        U5      :X  a  [        U5      nU$ ! [        [        4 a    [        U S35      ef = f)Nz must be a number.r   z must be a positive number.)Úfloatr~   r   r‚   )Úvaluer   ÚpositiveÚns       r(   Ú_numr    sp   € ð6Ü�%‹Lˆö �A˜“FÜ˜D˜6Ð!<Ð=Ó>Ð>ØŒC�‹Fƒ{Ü�‹FˆØ€Høô ”zÐ"ó 6Ü˜D˜6Ð!3Ð4Ó5Ð5ð6ús   ‚A ÁA#c                 ó
  • [        U [        5      (       a  U (       d  [        U S35      e/ nU  HP  n[        U5      R	                  5       R                  5       nU(       d  [        U S35      eUR                  U5        MR     U$ )Nz( must be a list with at least one entry.ú cannot contain an empty entry.)r¹   Úlistr   Ústrr¼   ÚlowerÚappend©r	  r   ÚoutÚitemrÑ   s        r(   Ú	_str_listr    st   € Ü�eœT×"Ñ"®%Ü˜D˜6Ð!IÐJÓKÐKØ
€CÛˆÜ�‹I�O‰OÓ×#Ñ#Ó%ˆÞÜ ˜vÐ%DÐEÓFÐFØ�
‰
�1Žñ	 ð
 €Jr*   ÚMotorwayÚCarwowÚAuction4CarszDealer AuctionÚ	DealerWay)r   r   r   r   r   r   r   c                 óò  • [         U   n[        U [        5      (       d  [        U S35      e[	        U R                  S5      U S35      [	        U R                  S5      U S35      [	        U R                  S5      U S35      S.n[	        U R                  S	5      U S
3SS9n[	        U R                  S5      U S3SS9nUS:  d  US:  a  [        U S35      eXE:”  a  [        U S35      eXEsUS	'   US'   U R                  S5      n[        U[        5      (       a  U(       d  [        U S35      e [        U Vs1 s H  n[        U5      iM     sn5      n[        S U 5       5      (       a  [        U S35      eXcS'   U[        ;   a#  [        U R                  S5      U S35      US'   U$ [	        U R                  S5      U S35      US'   U$ s  snf ! [        [        4 a    [        U S35      ef = f)a
  One platform's hard gate (mileage, reserve, age, grade, owners, plus
either a mile limit or, for Auction4Cars and Dealer Auction, an allow
list standing in for distance, depot name or county respectively), or
raise ValueError with a clear reason naming the platform.z"'s hard gate settings are missing.Úmax_mileagez max mileageÚmax_reservez max reserveÚ
max_ownersz max owners)r  r  r  Úmin_agez min ageF©r
  Úmax_agez max ager   z's age cannot be negative.z*'s min age cannot be greater than max age.Úallowed_gradesz, needs at least one condition grade allowed.z*'s condition grades must be whole numbers.c              3   óB   #   • U  H  oS :  =(       d    US:„  v •  M     g7f)r,   é   Nrè   )rb   Úgs     r(   re   Ú*_validate_platform_gate.<locals>.<genexpr>@  s   é € Ð
*¢6˜aˆq‰5�>�A˜‘EŒ>¢6ùó   ‚z,'s condition grades must be between 1 and 5.Úallowed_locationsz allowed locationsÚmax_distance_milesz max distance)Ú_PLATFORM_LABELSr¹   rº   r   r  r9   r  Úsortedr‚   r~   r½   Ú_LOCATION_LIST_PLATFORMSÚ_str_list_allow_empty)ÚpdÚplatform_keyr¦   r  r  r!  Úgradesr%  s           r(   Ú_validate_platform_gater1  $  s"  € ô
 ˜\Ñ*€EÜ�bœ$×ÑÜ˜E˜7Ð"DÐEÓFÐFä˜BŸF™F =Ó1°e°W¸LÐ3IÓJÜ˜BŸF™F =Ó1°e°W¸LÐ3IÓJÜ˜2Ÿ6™6 ,Ó/°E°7¸+Ð1FÓGñ€Cô
 �2—6‘6˜)Ó$¨¨¨xÐ&8À5ÑI€GÜ�2—6‘6˜)Ó$¨¨¨xÐ&8À5ÑI€GØ�ƒ{�g “kÜ˜E˜7Ð"<Ð=Ó>Ð>ØÓÜ˜E˜7Ð"LÐMÓNÐNØ%,Ð"€Cˆ	�N�C˜	‘Nà�V‰VÐ$Ó%€FÜ�fœd×#Ñ#®6Ü˜E˜7Ð"NÐOÓPÐPðOÜ©Ó0ª Aœ˜Qž©Ñ0Ó1ˆô Ñ
*¡6Ó
*×*Ñ*Ü˜E˜7Ð"NÐOÓPÐPØ"ÐÑàÔ/Ó/ô
 $9Ø�F‰FÐ&Ó'¨E¨7Ð2DÐ)Eó$GˆÐÑ ð €Jô %)¨¯©Ð0DÓ)EÈ%ÈÐP]ÐG^Ó$_ˆÐ Ñ!Ø€Jùò! 1øÜ”zÐ"ó OÜ˜E˜7Ð"LÐMÓNÐNðOús   Ä4	G Ä=GÅG ÇG ÇG6c                 óê   • U c  / n [        U [        5      (       d  [        U S35      e/ nU  HB  n[        U5      R	                  5       nU(       d  [        U S35      eUR                  U5        MD     U$ )Nz must be a list.r  )r¹   r  r   r  r¼   r  r  s        r(   r-  r-  P  ss   € Ø�}ØˆÜ�eœT×"Ñ"Ü˜D˜6Ð!1Ð2Ó3Ð3Ø
€CÛˆÜ�‹I�O‰OÓˆÞÜ ˜vÐ%DÐEÓFÐFØ�
‰
�1Žñ	 ð
 €Jr*   z^#[0-9a-fA-F]{6}$c                 ó‚   • [        U =(       d    S5      R                  5       n[        R                  U5      (       a  U$ U$ )aJ  A theme colour (Mark 2026-08-25: "give the option to set theme
colours"), from a native <input type=color> so always a real 6 digit
hex on a genuine save; falls back to the real default rather than
guessing on anything else (a cleared field, a stray string), never
breaks the page's own CSS with a malformed custom property value.r    )r  r¼   Ú_HEX_COLOR_REÚmatch)r	  r   ÚdefaultrÑ   s       r(   Ú
_hex_colorr7  a  s6   € ô 	ˆE�K�RÓ×ÑÓ €AÜ×#Ñ# A×&Ñ&ˆ1Ð3¨GÐ3r*   z\^data:image/(png|jpeg|jpg|svg\+xml|webp|x-icon|vnd\.microsoft\.icon);base64,[A-Za-z0-9+/=]+$iÀ‘! c                 óì   • [        U =(       d    S5      R                  5       nU(       d  g[        U5      [        :”  a  [	        U S35      e[
        R                  U5      (       d  [	        U S35      eU$ )a)  An uploaded logo or favicon (Mark 2026-08-25: "add logo upload for
the left panel, big and small also fav icon"), a real data URI read
client side from a genuine file picked in the browser via FileReader.
Empty means "use the real Right Drive default", always a valid save,
never guessed at. Rejects anything that is not a well formed image
data URI outright (never guess, and a malformed value would otherwise
break the very <img src="..."> or <link href="..."> it is placed
into) and anything too large to keep every page's own HTML a
reasonable size.r    z) is too large, keep it under about 1.5MB.z( does not look like a real image upload.)r  r¼   r=   Ú_DATA_IMAGE_MAX_LENr   Ú_DATA_IMAGE_REr5  )r	  r   rÑ   s      r(   Ú_data_imager;  p  sj   € ô 	ˆE�K�RÓ×ÑÓ €AÞØÜ
ˆ1ƒvÔ#Ó#Ü˜D˜6Ð!JÐKÓLÐLÜ×Ñ ×"Ñ"Ü˜D˜6Ð!IÐJÓKÐKØ€Hr*   c           	      ód  • 0 nU R                  S5      n[        U[        5      (       d  [        S5      e[         Vs0 s H  nU[        UR                  U5      U5      _M!     snUS'   [        U R                  S5      5      US'   [        U R                  S5      S5      US'   [        U R                  S5      =(       d    S5      R                  5       R                  5       US'   U R                  S5      nU(       a  [        US	5      OS
US'   U R                  S5      =(       d    SnUS;  a  [        S5      eXQS'   U R                  S5      nU(       a  [        US5      OS
US'   U R                  S5      nUb  [        USSS9OSUS'   US   S:  a  [        S5      eU R                  S5      nUb  [        U5      S;  a  [        S5      eUb  [        U5      OSUS'   U R                  S5      n	U	b  U	S:X  a  SUS'   O* [        U	5      n	SU	s=::  a  S ::  d  O  [        S5      eX‘S'   [        U R                  S!5      S"[        S!   5      US!'   [        U R                  S#5      S$[        S#   5      US#'   [        U R                  S%5      S&[        S%   5      US%'   [        U R                  S'5      S([        S'   5      US''   [        U R                  S)5      S*[        S)   5      US)'   [        U R                  S+5      S,5      US+'   [        U R                  S-5      S.5      US-'   [        U R                  S/5      S05      US/'   [        U R                  S15      S25      n
U
S3:”  a  [        S45      eX¡S1'   [        U R                  S55      S65      US5'   [!        U R                  S7S5      5      US7'   [#        U R                  S85      S95      US8'   [%        U R                  S:5      S;5      US:'   [%        U R                  S<5      S=5      US<'   [%        U R                  S>5      S?5      US>'   U R                  S@5      n[        U[&        5      (       d  [        SA5      e/ nU H¶  n[        U[&        [(        45      (       a  [+        U5      SB:w  a  [        SC5      e[        US   5      R                  5       R-                  5       [        US   5      R                  5       R-                  5       pþU(       a  U(       d  [        SD5      eUR/                  Xï/5        M¸     XÁS@'   U R                  SE5      n[        U[&        5      (       d  [        SF5      e/ nU GHT  n[        U[        5      (       a  UR                  SG5      (       d  [        SH5      e[        USG   5      R                  5       R-                  5       UR                  SI5      (       a*  [        USI   5      R                  5       R-                  5       OS
[        UR                  SJ5      SKUSG    35      SL.nUR                  SM5      =(       d    / nU(       a[  U Vs/ s HJ  n[        U5      R                  5       (       d  M#  [        U5      R                  5       R-                  5       PML     snUSM'   UR/                  U5        GMW     UUSE'   SNU ;   a  [1        U SN   5      USN'   SOU ;   a  [3        U SO   5      USO'   SPU ;   a  [5        U SP   5      USP'   SQU ;   a  [7        U SQ   5      USQ'   U$ s  snf ! [        [        4 a    [        S5      ef = fs  snf )Raå  Turn the raw JSON from the settings page into a clean settings dict
matching pricing.DEFAULT_SETTINGS' shape, or raise ValueError with a
clear reason. Make and model strings are lowercased here to match how
pricing.py's own _norm() compares them, so a value typed as "Ford" in the
page still matches a car read as "ford". The hard gate (mileage, reserve,
age, grade, owners, distance or depot list) is per platform (Mark
2026-08-20), everything else here stays shared across all three.Ú	platformsz,Missing the per platform hard gate settings.rŠ   Úsmall_car_cap_limitzSmall city car cap limitÚdealer_postcoder    Údefault_price_ceilingzPrice ceilingNÚdetails_styleÚdrawer)rB  Úpopupz&Details style must be drawer or popup.Úretail_price_capzRetail price caprL   zRetail est. marginFr   rM   r   z&Retail est. margin cannot be negative.rN   )rO   éc   z*Retail est. price ending must be 95 or 99.rO   Údealerkit_photosé   z=Photos sent to DealerKit must be a whole number from 1 to 20.r,   é   Útheme_bgzBackground colourÚtheme_pricezPrice colourÚ
theme_pillzPill colourÚ
theme_iconzIcon colourÚtheme_sidebarzSidebar colourÚtheme_logo_fullzThe full logoÚtheme_logo_smallzThe small logo markÚtheme_faviconzThe faviconÚretail_upliftzRetail upliftéd   z-Retail uplift over 100% looks like a mistake.Úflat_spreadzFlat spreadÚno_ferry_crossingÚferry_placeszFerry placesÚbanned_makeszBanned makesÚ
van_modelszVan and commercial model namesÚsmall_car_tokenszSmall city car model namesÚbanned_modelszBanned models must be a list.rS   z+Each banned model needs a make and a model.z0A banned model row is missing its make or model.Ú
value_capszValue caps must be a list.r2   z%Each value cap needs at least a make.r3   rû   zValue cap for )r2   r3   rû   ÚexceptÚpurchase_dropdownsÚpurchase_columnsÚpurchase_flowÚ	fee_bands)r9   r¹   rº   r   r*  r1  Ú_validate_platform_enabledr  r  r¼   Úupperr‚   r~   r7  r	   r;  r�   r-  r  r  Útupler=   r  r  Ú_validate_purchase_dropdownsÚ_validate_purchase_columnsÚ_validate_purchase_flowÚ_validate_fee_bands)Údr  Úplatforms_inr’   ÚceilingÚstyleÚ
retail_caprP   rQ   ÚphotosrQ  ÚmodelsÚclean_modelsrÌ   ÚmkÚtokÚcapsÚ
clean_capsÚcap_rowÚ
exceptionsÚes                        r(   Ú_validate_settingsrv  „  sÆ  € ð €Cà—5‘5˜Ó%€LÜ�l¤D×)Ñ)ÜÐGÓHÐHå#3ó5Ú#3˜Cð Ô4°\×5EÑ5EÀcÓ5JÈCÓPÒPÙ#3ñ5€CˆÑä8¸¿¹Ð?QÓ9RÓS€CÐÑä!% a§e¡eÐ,AÓ&BÐD^Ó!_€CÐÑô ! §¡Ð'8Ó!9×!?¸RÓ@×FÑFÓH×NÑNÓP€CÐÑð �e‰eÐ+Ó,€GÞEL¤4¨°Ô#AÐRV€CÐÑ ð
 �E‰E�/Ó"×. h€EØÐ'Ó'ÜÐAÓBÐBØ ˆÑð —‘Ð)Ó*€JÞFPœd :Ð/AÔBÐVZ€CÐÑð �U‰UÐ+Ó,€FØZ`ÑZl¤D¨Ð1EÐPUÒ$VÐru€CÐ Ñ!Ø
Ð#Ñ$ qÓ(ÜÐAÓBÐBØ�U‰UÐ+Ó,€FØÑœc &›k°Ó9ÜÐEÓFÐFØ39Ñ3E¤C¨¤KÈ2€CÐ Ñ!Ø�U‰UÐ%Ó&€FØ�~˜ 2›Ø"#ˆÐÒð	^Ü˜“[ˆFð �FÕ ˜bÕ ÜÐ\Ó]Ð]Ø"(ÐÑô ! §¡ zÓ!2Ð4GÜ"2°:Ñ">ó@€Cˆ
�Oä# A§E¡E¨-Ó$8¸.Ü%5°mÑ%DóF€CˆÑä" 1§5¡5¨Ó#6¸Ü$4°\Ñ$BóD€CˆÑä" 1§5¡5¨Ó#6¸Ü$4°\Ñ$BóD€CˆÑä% a§e¡e¨OÓ&<Ð>NÜ'7¸Ñ'HóJ€CˆÑä(¨¯©Ð/@Ó)AÀ?ÓS€CÐÑÜ)¨!¯%©%Ð0BÓ*CÐEZÓ[€CÐÑÜ& q§u¡u¨_Ó'=¸}ÓM€CˆÑä˜Ÿ™˜Ó/°ÓA€MØ�sÓÜÐHÓIÐIØ(ˆÑÜ˜aŸe™e MÓ2°MÓB€CˆÑô  $ A§E¡EÐ*=¸uÓ$EÓF€CÐÑÜ/°·±°nÓ0EÀ~ÓV€CˆÑä# A§E¡E¨.Ó$9¸>ÓJ€CˆÑÜ! !§%¡%¨Ó"5Ð7WÓX€CˆÑÜ'¨¯©Ð.@Ó(AÐC_Ó`€CÐÑà�U‰U�?Ó#€FÜ�fœd×#Ñ#ÜÐ8Ó9Ð9Ø€LÛˆÜ˜#¤¤e˜}×-Ñ-´°S³¸Q³ÜÐJÓKÐKÜ�c˜!‘f“+×#Ñ#Ó%×+Ñ+Ó-¬s°3°q±6«{×/@Ñ/@Ó/B×/HÑ/HÓ/JˆCÞžÜÐOÓPÐPØ×Ñ˜R˜IÖ&ñ ð (ˆÑà�5‰5�Ó€DÜ�dœD×!Ñ!ÜÐ5Ó6Ð6Ø€JÜˆÜ˜#œt×$Ñ$¨C¯G©G°F¯O©OÜÐDÓEÐEä˜˜F™Ó$×*Ñ*Ó,×2Ñ2Ó4Ø;>¿7¹7À7×;KÑ;K”c˜#˜g™,Ó'×-Ñ-Ó/×5Ñ5Ô7ÐQUÜ˜Ÿ™ ›¨.¸¸V¹¸Ð(FÓGñ
ˆð
 —W‘W˜XÓ&×,¨"ˆ
ÞÙAKÓ ^Â¸AÌsÐSTËvÏ|É|Ï~Ó!7¤ Q£§¡£×!5Ñ!5Ö!7ÁÑ ^ˆG�HÑØ×Ñ˜'×"ñ ð #€CˆÑð ˜qÓ Ü$@ÀÐCWÑAXÓ$YˆÐ Ñ!Ø˜QÓÜ"<¸QÐ?QÑ=RÓ"SˆÐÑØ˜!ÓÜ6°q¸Ñ7IÓJˆˆOÑØ�aÓÜ.¨q°©~Ó>ˆˆKÑà€JùòS5øôp œ:Ð&ó 	^ÜÐ\Ó]Ð]ð	^üò@ !_s   ¼&\	È \ Ø9"\-Ù+\-Ü\*>   ÚredÚskyÚblueÚgreyÚnavyÚpinkÚtealÚamberÚgreenÚteal2ÚorangeÚpurpleÚyellowc                 óf  • [        U [        5      (       d  [        S5      e0 nU R                  5        GHù  u  p#U[        R
                  ;  a  [        SU< 35      e[        U[        5      (       d  [        U S35      e/ [        5       SpenU GHR  n[        U[        5      (       a6  [        UR                  S5      =(       d    S5      R                  5       (       d  [        U S35      e[        US   5      R                  5       nUR                  5       U;   a  [        U SU< S	35      eUR                  UR                  5       5        [        UR                  S
5      =(       d    S5      n	U	[        ;  a  [        U SU	< 35      eX‰S.n
S H  nUR                  U5      (       d  M  SX«'   M!     XjR                  S5      (       a  SOS-  nUR                  U
5        GMU     US:”  a  [        U S35      eUS:X  a)  U(       a"  [        S U 5       5      (       d  [        S5      eXAU'   GMü     U$ )zðThe per column option lists. Only known columns, every option a non
empty string with a known pill colour, at most one default per column,
and stock_status must keep at least one finished option or nothing
could ever leave the working list.z$purchase_dropdowns must be an objectúUnknown dropdown column z: options must be a listr   r	  r    z: every option needs a valueú: ú appears twiceÚcolourrz  z: unknown colour ©r	  rˆ  )r6  Úfinishedr�   ÚretiredTr6  r,   z$: only one option can be the defaultÚstock_statusc              3   óB   #   • U  H  oR                  S 5      v •  M     g7f)rŠ  N)r9   )rb   r?   s     r(   re   Ú/_validate_purchase_dropdowns.<locals>.<genexpr>A  s   é € Ð6XÒRWÈQ·u±u¸Z×7HÐ7HÒRWùr'  zbStatus needs at least one option that means finished, or nothing could ever leave the working list)r¹   rº   r   r¾   r   ÚPURCHASE_DROPDOWN_DEFAULTSr  rl   r  r9   r¼   r  ÚaddÚ_PILL_COLOURSr  r½   )Úddr  ÚcolÚoptionsÚcleanÚseenÚdefaultsÚor	  rˆ  rÌ   Úflags               r(   rc  rc  !  s÷  € ô
 �bœ$×ÑÜÐ?Ó@Ð@Ø
€CØŸ™Ÿ
‰ˆØ”b×3Ñ3Ó3ÜÐ7¸±wÐ?Ó@Ð@Ü˜'¤4×(Ñ(Ü ˜uÐ$<Ð=Ó>Ð>Ø "¤C£E¨1�XˆÜˆAÜ˜a¤×&Ñ&¬c°!·%±%¸³.×2FÀBÓ.G×.MÑ.M×.OÑ.OÜ  C 5Ð(DÐ!EÓFÐFÜ˜˜'™
“O×)Ñ)Ó+ˆEØ�{‰{‹} Ó$Ü  C 5¨¨5©)°>Ð!BÓCÐCØ�H‰H�U—[‘[“]Ô#Ü˜Ÿ™˜x›×2¨FÓ3ˆFØœ]Ó*Ü  C 5Ð(9¸&¹Ð!DÓEÐEØ!Ñ4ˆCÛB�Ø—5‘5˜—;“;Ø $�C“Iñ Cð ŸW™W Y×/Ñ/™°QÑ6ˆHØ�L‰L˜×ñ ð  �a‹<Ü ˜uÐ$HÐIÓJÐJØ�.Ó ¦U´3Ñ6XÑRWÓ6X×3XÑ3XÜð Ló Mð MàˆCŒñ7 #ð8 €Jr*   c                 óv  • [        U [        5      (       d  [        S5      e0 nU R                  5        GH  u  p#[        U[        5      (       d  [        U S35      e[	        UR                  SS5      5      [	        UR                  SS5      5      S.nUR                  S5      b=  [        US   U S35      nS	Us=::  a  S
::  d  O  [        U S35      e[        U5      US'   UR                  S5      b<  [        US   U S3SS9nSUs=::  a  S::  d  O  [        U S35      e[        U5      US'   XAU'   GM     U$ )z½Per column: table and record visibility, plus, since the
2026-08-27 polish pass, an optional dragged width (w, pixels) and an
optional drag reorder position (pos). Anything else is dropped.z"purchase_columns must be an objectz: visibility must be an objectÚtableTÚrecord)r›  rœ  rÕ   z widthé(   i„  z): width must be between 40 and 900 pixelsÚposz	 positionFr   r   rE  z#: position must be between 0 and 99)r¹   rº   r   r¾   r�   r9   r  r‚   )Úpcr  r“  Úvisr•  rÕ   rž  s          r(   rd  rd  H  s0  € ô �bœ$×ÑÜÐ=Ó>Ð>Ø
€CØ—H‘H—J‰ˆÜ˜#œt×$Ñ$Ü ˜uÐ$BÐCÓDÐDÜ˜sŸw™w w°Ó5Ó6Ü §¡¨°$Ó 7Ó8ñ:ˆà�7‰7�3‹<Ñ#Ü�S˜‘X #  f˜~Ó.ˆAØ˜•>˜c•>Ü  C 5Ð(QÐ!RÓSÐSÜ˜Q›ˆE�#‰JØ�7‰7�5‹>Ñ%Ü�s˜5‘z c U¨)Ð#4¸uÑEˆCØ˜•>˜r•>Ü  C 5Ð(KÐ!LÓMÐMÜ˜s›8ˆE�%‰LØˆCŒñ ð  €Jr*   c                 ót  • [        U [        5      (       d  [        S5      eS[        U R	                  S5      5      0nU R	                  S5      =(       d    0 n[        U[        5      (       d  [        S5      eUR                  5        VVs0 s H  u  p4[        U5      [        U5      _M     snnUS'   U$ s  snnf )Nzpurchase_flow must be an objectÚvideo_stageÚfee_rebandszfee_rebands must be an object)r¹   rº   r   r�   r9   r¾   r  )Úpfr  Úrebandsr¬   r­   s        r(   re  re  b  s—   € Ü�bœ$×ÑÜÐ:Ó;Ð;Øœ$˜rŸv™v mÓ4Ó5Ð
6€CØ�f‰f�]Ó#×) r€GÜ�gœt×$Ñ$ÜÐ8Ó9Ð9Ø6=·m±m´oÔF²o©d¨aœ#˜a›&¤$ q£'š/±oÒF€CˆÑØ€Jùó Gs   Â
!B4c           
      ó  • [        U [        5      (       d  [        S5      e[        U 5      S:”  a  [        S5      eSn/ [	        5       p2U  GH#  n[        U[
        5      (       a6  [        UR                  S5      =(       d    S5      R                  5       (       d  [        S5      e[        US   5      R                  5       SS	 nUR                  5       U;   a  [        U< S
35      eUR                  UR                  5       5        UR                  S5      =(       d    0 n[        U[
        5      (       d  [        U S35      e0 nU H0  nX†;   d  M
  Xh   S;  d  M  US:X  a  Xh   O[        Xh   5      SS Xx'   M2     UR                  S5      n	U	S;   aG  UR                  S5      S;  a2  US   n
[        U
[        5      (       a  U
O[        R                  " U
5      n	[        U	[        5      (       aT  U	R                  5       (       a?   [        R                  " U	5      n[        U[
        5      (       a  [        U	5      S::  a  X—S'   UR                  XW[        UR                  S5      5      S.5        GM&     U$ ! [         a    Sn Nif = f)zÉThe purchases page's own saved views: a list of named filter and
sort snapshots. Only the known criteria keys survive, anything else a
stale or hand built payload carries is dropped rather than stored.zpurchase_views must be a listr�  úthat is too many saved views)ÚviewÚtabÚsearchÚfromÚtoÚsortKeyÚsortDirÚfilterstatusÚfiltercancelreasonÚdatefromÚdatetoÚcollfromÚcolltoÚfiltervideoÚfilterpersonÚfilterfinanceÚfilterlogbookÚ
filterpaidÚ
filterdocsÚfilterpriceminÚfilterpricemaxÚfilterretailminÚfilterretailmaxÚfiltermarginminÚfiltermarginmaxÚfiltermilesminÚfiltermilesmaxr   r    úevery view needs a nameNry   r‡  Úcriteriaú: criteria must be an object©Nr    r®  éx   Úcolumnsr]  ip  r`   ©r   rÄ  r`   )r¹   r  r   r=   rl   rº   r  r9   r¼   r  r�  ri   ÚdumpsÚloadsr  r�   )ÚpvÚkeepr  r–  r­   r   Úcritr•  r¬   ÚcolsrŸ  Úparseds               r(   Ú_validate_purchase_viewsrÑ  m  s!  € ô �bœ$×ÑÜÐ8Ó9Ð9Ü
ˆ2ƒw�ƒ|ÜÐ7Ó8Ð8ðV€Dð ”C“EˆÜˆÜ˜!œT×"Ñ"¬#¨a¯e©e°F«m×.A¸rÓ*B×*HÑ*H×*JÑ*JÜÐ6Ó7Ð7Ü�1�V‘9‹~×#Ñ#Ó% c rÐ*ˆØ�:‰:‹<˜4ÓÜ ™x ~Ð6Ó7Ð7Ø�‰�—‘“ÔØ�u‰u�ZÓ ×& BˆÜ˜$¤×%Ñ%Ü ˜vÐ%AÐBÓCÐCØˆÛˆAØ�y˜T™W¨JÕ6Ø&'¨9£n˜4š7¼#¸d¹g»,ÀtÈÐ:L�“ñ ð �x‰x˜	Ó"ˆØ�:Ó $§(¡(Ð+=Ó">ÀjÓ"PØÐ(Ñ)ˆBÜ# B¬×,Ñ,‘2´$·*²*¸R³.ˆDÜ�dœC× Ñ  T§Z¡Z§\¡\ðÜŸš DÓ)�ô ˜&¤$×'Ñ'¬C°«I¸Ó,=Ø#'�iÑ ð 	�
‰
˜DÄÀQÇUÁUÈ9ÓEUÓ@VÑW×Xñ? ð@ €Jøô ó Ø’ðús   ÈI4É4JÊJc           
      óZ  • [        U [        5      (       d  [        S5      e[        U 5      S:”  a  [        S5      eSn/ [	        5       p2U  GH\  n[        U[
        5      (       a6  [        UR                  S5      =(       d    S5      R                  5       (       d  [        S5      e[        US   5      R                  5       SS	 nUR                  5       U;   a  [        U< S
35      eUR                  UR                  5       5        UR                  S5      =(       d    0 n[        U[
        5      (       d  [        U S35      e0 nU H0  nX†;   d  M
  Xh   S;  d  M  US:X  a  Xh   O[        Xh   5      SS Xx'   M2     UR                  XW[        UR                  S5      5      S.5        GM_     U$ )a9  Didn't win's own saved views (2026-08-30, the last _slice_chip
holdout brought onto the one views control). Same shape as
_validate_purchase_views just above, a smaller page with a smaller
keep list: this page's own real filters, nothing more. Last 30 days
used to be a fixed slice; it is now the ordinary filterrecentonly
checkbox below, saved and restored like any other filter, but always
RE-COMPUTED as a rolling window from today when it is next applied,
never a fixed date. Lost by under GBP 250 needed no new key at all,
it is just the existing lostbymax filter.zlost_bids_views must be a listr�  r§  )
ÚbidfromÚbidtoÚ	lostbyminÚ	lostbymaxÚ
soldforminÚ
soldformaxrª  Úfilterrecentonlyr­  r®  r   r    rÃ  Nry   r‡  rÄ  rÅ  rÆ  r®  rÇ  r`   rÉ  )r¹   r  r   r=   rl   rº   r  r9   r¼   r  r�  r  r�   )	ÚlvrÍ  r  r–  r­   r   rÎ  r•  r¬   s	            r(   Ú_validate_lost_bids_viewsrÛ  ª  sn  € ô �bœ$×ÑÜÐ9Ó:Ð:Ü
ˆ2ƒw�ƒ|ÜÐ7Ó8Ð8ð"€Dð ”C“EˆÜˆÜ˜!œT×"Ñ"¬#¨a¯e©e°F«m×.A¸rÓ*B×*HÑ*H×*JÑ*JÜÐ6Ó7Ð7Ü�1�V‘9‹~×#Ñ#Ó% c rÐ*ˆØ�:‰:‹<˜4ÓÜ ™x ~Ð6Ó7Ð7Ø�‰�—‘“ÔØ�u‰u�ZÓ ×& BˆÜ˜$¤×%Ñ%Ü ˜vÐ%AÐBÓCÐCØˆÛˆAØ�y˜T™W¨JÕ6Ø&'¨9£n˜4š7¼#¸d¹g»,ÀtÈÐ:L�“ñ ð 	�
‰
˜DÄÀQÇUÁUÈ9ÓEUÓ@VÑW×Xñ ð €Jr*   c                 óF  • [        U [        5      (       d  [        S5      e0 nU R                  5        Hê  u  p#[        U[        5      (       a  U(       d  [        U S35      e/ nU H¯  n[        U[        [
        45      (       a  [        U5      S:w  a  [        U S35      e[        US   U S3SS9n[        US	   U S
3SS9n[        US   U S35      nUS:  d  US:  a  [        U S35      eXg:”  a  [        U S35      eUR                  XgU/5        M±     XAU'   Mì     U$ )z£A platform's whole band table replaces the factory one, so it must
be complete and sane on its own: [low, high, fee] rows, numbers, low
at most high, fee positive.zfee_bands must be an objectz): the band table must be a non empty listé   z: each band is [low, high, fee]r   z	 band lowFr   r,   z
 band highrS   z	 band feez: a band cannot be negativez(: a band's low end is above its high end)	r¹   rº   r   r¾   r  rb  r=   r  r  )	Úfbr  ÚplatÚbandsr•  rÌ   ÚloÚhiÚfees	            r(   rf  rf  Î  s6  € ô �bœ$×ÑÜÐ6Ó7Ð7Ø
€CØ—x‘x–z‰ˆÜ˜%¤×&Ñ&®eÜ ˜vÐ%NÐOÓPÐPØˆÛˆCÜ˜c¤D¬% =×1Ñ1´S¸³XÀ³]Ü  D 6Ð)HÐ!IÓJÐJô �c˜!‘f   iÐ0¸5ÑAˆBÜ�c˜!‘f   jÐ1¸EÑBˆBÜ�s˜1‘v $  yÐ1Ó2ˆCØ�A‹v˜˜a›Ü  D 6Ð)DÐ!EÓFÐFØ‹wÜ  D 6Ð)QÐ!RÓSÐSØ�L‰L˜" #˜Ö'ñ ð ˆD‹	ñ# "ð$ €Jr*   c                 óf   • [         R                  U 5      nUS L=(       a    UR                  5       S L $ ©N)Ú_procsr9   Úpoll)r’   Úps     r(   r¨   r¨   ê  s)   € Ü�
‰
�3‹€AØ�Dˆ=×-˜QŸV™V›X¨Ð-Ð-r*   c                  ó.   • [        S [         5       5      $ )zßWhether anything this server started is still alive. Every pass,
headed or not, shares one rule (bidbrain.browser.open_context's own
docstring: only one process may use the profile at a time), and a
login window counts too.c              3   ó8   #   • U  H  n[        U5      v •  M     g 7frå  )r¨   ©rb   r¬   s     r(   re   Ú!_any_proc_busy.<locals>.<genexpr>ô  s   é € Ð+¢F˜qŒx˜�{ˆ{¢Fùs   ‚)r½   ræ  rè   r*   r(   Ú_any_proc_busyrí  ï  s   € ô
 Ñ+¥FÓ+Ó+Ð+r*   c                 ó  • X;   a  g[         R                  R                  U 5      nUb  UR                  (       a  g/ nU HI  n[         R                  R                  U5      nUb  UR                  (       d    gUR	                  U5        MK     U$ )a!  Which quiet background passes to stop so `key` can start right now
(Steven 2026-09-08: "anytime you need a run to happen just force it to
happen straight away"). The automatic DealerKit stage check is quiet
and restartable, the stage loop simply runs it again a few minutes
later, so it gives way to a real run. None when the request must wait:
something that is not quiet is running (a daily run, a login window,
a headed check), the same run is already running, or the request is
itself a quiet pass, which always waits its turn. Pure, tested.N)r�   rŽ   r9   Úquietr  )r’   Úrunning_keysÚaskedÚstopr¬   r•   s         r(   Ú_quiet_in_the_wayró  ÷  sn   € ð ÓØÜ�J‰J�N‰N˜3Ó€EØ�}˜ŸŸØØ€DÛˆÜ�j‰j�n‰n˜QÓˆØ‰;˜cŸiŸiÙØ�‰�AŽñ	 ð
 €Kr*   c                 óð   • SSK n [        R                  " [        R                  " U R                  5      UR
                  5        g! [         a%     U R                  5          g! [         a      gf = ff = f)z‚Kill a pass and its own browser: the whole process group it was
started in, or the process alone when the group cannot be reached.r   NTF)Úsignalrï   ÚkillpgÚgetpgidÚpidÚSIGTERMrÞ   Ú	terminate)Úprocrõ  s     r(   Ú
_terminaterü    sa   € ó ðÜ
�	Š	”"—*’*˜TŸX™XÓ&¨¯©Ô7ØøÜó ð	Ø�N‰NÔÙøÜó 	Úð	úð	ús)   †?A Á
A5ÁA#Á#
A1Á-A5Á0A1Á1A5c                  óÊ   • [        S5      (       a  [        (       a
  S[         S3$ S$ [         H2  n [        U 5      (       d  M  [        R                  U    R
                  s  $    g)z~A plain English reason for _any_proc_busy(), from the registry, or
None when nothing is busy. The login window names its site.r£   r    r¡   r¢   N)r¨   rî   ræ  r�   rŽ   Úbusy)r’   s    r(   rõ   rõ     sU   € ô �×Ñß?Jº{Ð,¬[¨M¸Ð;ÐiÐPiÐißˆÜ�C�=‹=Ü—:‘:˜c‘?×'Ñ'Ò'ñ ð r*   c                 ó¶  • U R                  SS5      nU R                  S5      =(       d    SR                  5       R                  5       nSSS.R                  U5      nU R                  S5      nU(       a  U(       a  U(       d  g[        U R                  S	S
5      5      n[        R
                  " XXE5        US:X  až  [        [        R                  " 5       R                  S5      =(       d    / 5      n[        R                  " 5       =(       d    0 R                  S5      =(       d    0 nSU;  a,  UR                  S5      S
L a  [        R
                  " USXE5        g
)a#  Queue the platform side of a star or unstar: the one platform action
BidBrain is ever allowed to take (Mark 2026-08-27: "when we shortlist a
car in bidbrain add that to watched/tracked in auction sites", approved
by Steven 2026-06-09, shortlist only, never a bid). Only Motorway and
Carwow have a real write built; any other platform's star still saves
locally, with nothing to sync out. reg alone is not enough to find the
car's own page, a real listing_url is needed, so a card with none is
skipped, never guessed at. d is the cockpit's cardData() dict (its
lowercase data-platform key, "motorway"/"carwow", mapped to the exact
"Motorway"/"Carwow" the watchlist table keys on) or the same shape
arriving from Dealer OS (2026-09-02), which is why this is a module
function the sync thread can call too.ra   r    Úplatformr  r  ©r   r   Úlisting_urlFÚonTr‰   r   rµ   zMotorway (accounts))	r9   r¼   r  r�   r   Úqueue_watchlist_syncrl   rU   r  )rg  ra   Úplat_keyr   r  Úwatchedr”   r  s           r(   Úqueue_star_watchlistr  (  s	  € ð �%‰%��rÓ
€CØ—‘�jÓ!×' R×.Ñ.Ó0×6Ñ6Ó8€HØ&°(Ñ;×?Ñ?ÀÓI€HØ—%‘%˜Ó&€KÞ–H¦ØÜ�1—5‘5˜˜tÓ$Ó%€GÜ×Ò˜C¨;Ô@ð �:ÓÜ”R—_’_Ó&×*Ñ*Ð+?Ó@×FÀBÓGˆÜ×$Ò$Ó&×,¨"×1Ñ1Ð2EÓF×LÈ"ˆØ fÓ,°·±¸DÓ1AÀTÒ1IÜ×#Ò# CÐ)>ÀÔUØr*   Fc                  ó2   • Sq [        R                  " 5       qg )NT)Ú_dealer_os_dirtyrÝ   Ú_dealer_os_dirty_atrè   r*   r(   Ú_dealer_os_mark_dirtyr  d  s   € àÐÜŸ)š)›+Ñr*   >   ú/api/settingsú/api/record-chipú/api/cancel-purchaseú/api/check-in-purchaseú/api/set-purchase-fieldú/api/add-manual-purchaseú/api/set-collection-dateú/api/set-retail-estimateú/api/add-purchase-commentú/api/integration-settingsú/api/delete-manual-purchaseú/api/purchase-page-settingsú/api/lost-bids-page-settingsú!/api/add-purchase-dropdown-optionrŒ  Ú
on_financeÚvideo_statusÚperson_collectingÚlocationÚlogbook_stateÚsupplier)rŒ  r  r  r  r  r  r  c                 ó   • U R                  S5      nU R                  S5      nU(       a  U(       d  [        S5      eU R                  S5      nU(       a  SU0O0 nU[        ;   aY  US;  aS  [        R                  " S0 UD6R                  [        U   / 5       Vs1 s H  owS   iM	     nnXX;  a  [        U< S35      eUS:X  a2  [        R
                  " X54S	U0UD6n	U	S
:X  a  U(       d  [        X15        g[        R                  " X4U4S	U0UD6  US:X  a5  [        R                  " US40 UD6  U(       d  US;  a  [        USU5        ggggs  snf )aÕ  The one door for editing a purchase field, used by the Mac's own page
(POST /api/set-purchase-field) and by a purchase.set action pulled from
Dealer OS (2026-09-02). The editable whitelist lives in
db._PURCHASE_EDITABLE_FIELDS; anything else (a platform read figure) is
refused there, never silently accepted. A dropdown field's value must
be one of that column's own configured options (or empty), so a stale
page cannot write a value the dealership has since removed.ÚidÚfieldzid and field are requiredr	  rð   rÆ  z% is not one of the configured optionsrŒ  ÚwhoÚcheckinNÚtransport_feeÚ	transportrè   )
r9   r   Ú_PURCHASE_DROPDOWN_COLSr   Úget_purchase_dropdownsÚset_stock_statusÚ_dealerkit_checkin_halfÚset_purchase_fieldÚclear_dk_stampÚ_dealerkit_followup_now)
rg  r#  rð   rø  r"  r	  Úkwr˜  ÚallowedÚwhats
             r(   Úapply_purchase_fieldr1  �  sR  € ð �%‰%�‹+€CØ�E‰E�'‹N€EÞ–eÜÐ4Ó5Ð5Ø�E‰E�'‹N€EÞˆ&�$‰ R€BØÔ'Ó'¨E¸Ó,Cä×,Ò,Ñ2¨rÑ2×6Ñ6Ô7NÈuÑ7UÐWYÔZó\ÚZð #$�W”:ÙZð 	ð \àÓÜ ™yÐ(MÐNÓOÐOØ�Óô
 ×"Ò" 3Ñ=°3Ð=¸"Ñ=ˆØ�9Ó¦TÜ# CÔ-ØÜ×Ò˜# eÑ;°Ð;¸Ò;Ø�Óô 	×Ò˜#˜{Ñ1¨bÒ1Þ˜ ZÓ/Ü# C¨°cÕ:ð 0ˆtð	  ùò\s   ÂEc                  ó.   •  " S S[         5      n U " 5       $ )a˜  The request handler's own purchase doors (record a chip, cancel,
check in, add by hand, the DealerKit row jobs) usable from the Dealer
OS sync thread, with no socket behind them. Same methods, same checks,
same refusals: an action pulled from Dealer OS can never do anything a
click on the Mac's own page could not (2026-09-03). Built on first use
because the handler class is defined further down this file.c                   ó   • \ rS rSrS rSrg)z_Doors.<locals>.Doorsi­  c                 ó   • g rå  rè   ©Úselfs    r(   Ú__init__Ú_Doors.<locals>.Doors.__init__®  s   € Ør*   rè   N)Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__r7  Ú__static_attributes__rè   r*   r(   ÚDoorsÚ_Doors.<locals>.Doors­  s   † õ	r*   r>  )ÚCockpitHandler)r>  s    r(   Ú_DoorsrA  ¦  s   € ô”ô ñ ‹7€Nr*   c                 óZ   • U S:X  a  S[         4$ U S:X  a  S[        4$ [        SU < S35      e)NÚ	purchasesÚpurchase_viewsÚlostÚlost_bids_viewszunknown page z, expected purchases or lost)rÑ  rÛ  r   )Úpages    r(   Ú
_views_keyrH  ³  s>   € Øˆ{ÓØÔ!9Ð9Ð9Øˆvƒ~Ø Ô";Ð;Ð;Ü
�} T¡HÐ,HÐIÓ
JÐJr*   c           	      ó¤  • [        U 5      u  pEU=(       d    SR                  5       nU(       d  [        S5      e[        U=(       d    0 5      nUR	                  SS5      nUS;  a0  [        U[        5      (       a  UO[        R                  " U5      US'   [        R                  " 5       R                  U5      =(       d    /  Vs/ s HB  n[        UR                  SS5      5      R                  5       UR                  5       :w  d  M@  UPMD     n	nU	R                  X[        U5      S.5        U" U	5      n
[        R                  " XJ05        U
$ s  snf )	zÂviews.save from Dealer OS: upsert one named view into the page's own
saved views list through the same validator the Mac's own page saves
through, so a view saved there applies here identically.r    za view needs a namer]  NrÆ  rÈ  r   rÉ  )rH  r¼   r   rº   Úpopr¹   r  ri   rÊ  r   rU   r9   r  r  r�   Úset_settings)rG  r   rÄ  r`   r’   ÚvalidaterÎ  rÏ  r­   Úviewsr•  s              r(   Úsave_view_actionrN  »  s
  € ô ˜tÓ$�M€CØ�J�B×ÑÓ€DÞÜÐ.Ó/Ð/Ü�—˜BÓ€DØ�8‰8Ð&¨Ó-€DØ�:ÓÜ",¨T´3×"7Ñ"7™$¼T¿ZºZÈÓ=MˆˆY‰ÜŸšÓ*×.Ñ.¨sÓ3×9°rÐ9ÓmÒ9�1¼cÀ!Ç%Á%ÈÐPRÓBSÓ>T×>ZÑ>ZÓ>\Ð`d×`jÑ`jÓ`lÑ>l�QÑ9€EÐmØ	‡L�L˜$¼TÀ'»]ÑKÔLÙ�U‹O€EÜ‡O‚O�S�LÔ!Ø€Lùò	 ns   Ã?EÄEc           	      óà  • [        U 5      u  p#[        R                  " 5       R                  U5      =(       d    / nU Vs/ s HY  n[	        UR                  SS5      5      R                  5       U=(       d    SR                  5       R                  5       :w  d  MW  UPM[     nn[        U5      [        U5      :X  a  [        SU< SU  S35      e[        R                  " X#" U5      05        U$ s  snf )Nr   r    zno saved view called z on the z page)
rH  r   rU   r9   r  r  r¼   r=   r   rK  )rG  r   r’   rL  rM  r­   Úkepts          r(   Údelete_view_actionrQ  Î  s¿   € Ü˜tÓ$�M€CÜ�OŠOÓ×!Ñ! #Ó&×,¨"€EÙÓ_’u�!¤ A§E¡E¨&°"Ó$5Ó 6× <Ñ <Ó >À4Ç:È2×BTÑBTÓBV×B\ÑB\ÓB^Ñ ^�A‘u€DÐ_Ü
ˆ4ƒy”C˜“JÓÜÐ0°±¸ÀÀÀeÐLÓMÐMÜ‡O‚O�S˜( 4›.Ð)Ô*Ø€Kùò	 `s   ¿AC+ÂC+Úpricerl  Ú	documentsÚretailÚchipr&  r$  ÚremoveÚfigures)	rR  rl  rS  rT  rU  r&  r$  rV  rW  c                 ó
  • U R                  S5      =(       d    Sn/ nU R                  S5      (       a  UR                  U S35        U R                  S5      (       a/  UR                  U S   R                  S5      =(       d    U S35        U R                  S5      (       d  UR                  U S	35        U R                  S
5      c  UR                  U S35        U R                  S5      c  UR                  U S35        U R                  S5      (       d  / (       d  UR                  U S35        U R                  S5      c  UR                  U S35        U R                  S5      (       d  UR                  U S35        U$ )a  Why this car cannot be sent whole to DealerKit yet (purchase.dk_send,
2026-09-05): one plain sentence per missing thing, an empty list when
everything is there. Dealer OS shows these sentences as they are.
Pure, so the automatic DealerKit run and the tests use the same rule
as the door.ra   úThis carÚcancelled_atú& is a cancelled sale, nothing to send.Ú
double_winÚholdz. was won twice. Cancel one of the sales first.r-   úY has no mileage on file. Enter the mileage first, DealerKit is never allowed to guess it.Ú	car_pricez# has no purchase price on file yet.Úeffective_feezG has no buyer's fee on file yet. Enter the fee, zero if there was none.Ú
photo_urlsz has no photos on file yet.rR   z8 has no retail estimate yet. Set the retail price first.Úcollection_datez? has no collection date yet. Confirm the collection date first.)r9   r  )rÌ   ra   r  s      r(   Údk_send_preflightrc  å  s`  € ð �'‰'�%‹.×
&˜J€CØ
€CØ
‡w�wˆ~×ÑØ�
‰
�c�UÐ@ÐAÔBð ‡w�wˆ|×ÑØ�
‰
�3�|Ñ$×(Ñ(¨Ó0÷ MØ˜ÐKÐLô	Nà�7‰7�9×ÑØ�
‰
�c�Uð =ð =ô 	>à
‡w�wˆ{ÓÑ#Ø�
‰
�c�UÐ=Ð>Ô?Ø
‡w�wˆÓÑ'Ø�
‰
�c�UÐaÐbÔcØ�G‰G�L×!Ñ!¦RØ�
‰
�c�UÐ5Ð6Ô7Ø
‡w�wÐ Ó!Ñ)Ø�
‰
�c�UÐRÐSÔTØ�7‰7Ð$×%Ñ%Ø�
‰
�c�UÐYÐZÔ[Ø€Jr*   c                 ó  • U R                  S5      =(       d    Sn[        R                  " U 5      (       d  U S3$ US:X  aÍ  U R                  S5      (       d  U S3$ U=(       d    S H¢  nUR                  S5      U R                  S5      :w  d  M)  UR                  S5      =(       d    S	R                  S
S	5      R	                  5       UR                  S
S	5      R	                  5       :X  d  M…  UR                  S5      (       a  M�  U S3s  $    gU R                  S5      (       a  U S3$ US:X  a  U R                  S5      (       d  U S3$ US:X  aB  U(       d;  [        R
                  " U 5      (       d   [        R                  " U 5      (       d  U S3$ US:X  a  U R                  S5      c  U S3$ US:X  a  U R                  S5      (       d  U S3$ US:X  a  U R                  S5      c  U S3$ US:X  a5  [        R                  " U R                  S5      5      (       a  [        U 5      $ US:X  a_  U R                  S5      =(       d    0 R                  S5      b5  [        R                  " U R                  S5      5      (       d  [        U 5      $ g)a7  Why a DealerKit follow up (chip, transport, checkin, remove) cannot
be sent for this row, as one plain sentence, or None when it can.
Every follow up needs the car to be on DealerKit already; a remove
needs a cancelled sale, the other three need a live one. rows (the
whole purchases list, when the caller has it) lets a remove refuse
when another LIVE purchase shares the registration: DealerKit is
found by registration, so removing the cancelled sale's record would
remove the live sale's (the Ford Puma, sold on both platforms the
same day, is exactly this case).ra   rY  z& is not on DealerKit, nothing to send.rV  rZ  zH is not cancelled. Cancel the sale first, then remove it from DealerKit.rè   r!  r    r¸   zb was also bought as a live sale that is still on the list, so its DealerKit record was left alone.Nr[  rU  Úchipsz' has no chip recorded, nothing to send.rW  z<'s price and fee on DealerKit already match what is on file.r&  r%  z/ has no transport fee on file, nothing to send.r$  Úchecked_in_atz) has not been checked in on BidBrain yet.rT  rR   z1 has no retail estimate on file, nothing to send.Údk_checkÚlife_cycle_status)r9   r   Úon_dealerkitÚreplacera  Údk_figures_driftÚdk_figures_missingr   Úprice_lockedÚdk_price_locked_wordsÚprice_syncable)rÌ   ÚactionrI   rñ  ra   Úothers         r(   Údk_followup_refusalrr  	  sd  € ð �'‰'�%‹.×
&˜J€CÜ�?Š?˜3×ÑØ�Ð<Ð=Ð=Ø�ÓØ�w‰w�~×&Ñ&Ø�UÐbÐcÐcØ—Z˜R’ZˆEØ�y‰y˜‹ #§'¡'¨$£-Õ/°U·Y±Y¸uÓ5E×5KÈ×4TÑ4TÐUXÐZ\Ó4]×4cÑ4cÓ4eÐil×itÑitÐuxÐz|Ói}÷  jDñ  jDó  jFõ  5FØ!ŸI™I n×5Ó5Ø˜%ð  ;ð ;ò <ñ  ð
 Ø
‡w�wˆ~×ÑØ�Ð<Ð=Ð=Ø�Ó §¡¨× 0Ñ 0Ø�Ð=Ð>Ð>Ø�Ó¦5´×1DÒ1DÀS×1IÑ1IÔRT×RgÒRgÐhk×RlÑRlð �ÐRÐSÐSØ�Ó §¡¨Ó!9Ñ!AØ�ÐEÐFÐFØ�Ó 3§7¡7¨?×#;Ñ#;Ø�Ð?Ð@Ð@ð �Ó˜cŸg™gÐ&7Ó8Ñ@Ø�ÐGÐHÐHØ�Óœo×:Ò:¸3¿7¹7À:Ó;N×OÑOÜ$ SÓ)Ð)Ø�Ó˜sŸw™w zÓ2×8°b×=Ñ=Ð>QÓRÑ^Ü#×2Ò2°3·7±7¸:Ó3F×GÑGÜ$ SÓ)Ð)Ør*   c                 ó,  • U R                  S5      =(       d    0 nUR                  S5      =(       d    SnU R                  S5      =(       d    Sn[        R                  " U5      (       d  UR                  S5      b  U SU S3$ US	:X  a  SOUnU SU S
3$ )z¿The one sentence for a price refused because it is not ours to
send (2026-09-07, Steven): Ordered or Sold on DealerKit, where the
price is DealerKit's alone, or Non-Stock, a car not for sale.rg  ÚstageÚOrderedra   rY  rh  z is z> on DealerKit, not a car for sale, so its price is left alone.zAwaiting DeliveryzK on DealerKit, so its price is DealerKit's alone and is not sent from here.)r9   r   ro  )rÌ   Údcrt  ra   rÎ   s        r(   rn  rn  ;  s’   € ð 
�‰�Ó	×	" €BØ�F‰F�7‹O×(˜y€EØ
�'‰'�%‹.×
&˜J€CÜ×)Ò)¨"×-Ñ-°"·&±&Ð9LÓ2MÑ2YØ��d˜5˜'Ð!_Ð`Ð`ØÐ!4Ó4‰9¸%€DØˆU�$�t�fÐgÐhÐhr*   c                 ó’   ^ ^• [        UU 4S j[         5       5      (       d  [        R                  T TU45        [        [        5      $ )Nc              3   óJ   >#   • U  H  u  po1T:H  =(       a    UT:H  v •  M     g 7frå  rè   )rb   rè  rc   Ú_rp  rø  s       €€r(   re   Ú!dk_waiting_add.<locals>.<genexpr>R  s#   øé € ÐC²{©G¨A°!�C‰x×'˜A ™KÔ'²{ùs   ƒ #)r½   Ú_DK_WAITINGr  r=   )rø  rp  r#  s   `` r(   Údk_waiting_addr|  Q  s4   ù€ ÜÕCµ{ÓC×CÑCÜ×Ñ˜C ¨Ð-Ô.ÜŒ{ÓÐr*   c                  ó6   • [        [        5      n [        S S 2	 U $ rå  )r  r{  )r  s    r(   Údk_waiting_taker~  W  s   € Ü
Œ{Ó
€CÜ’AˆØ€Jr*   c            
      óà   • [        5        H5  u  pn US:X  a  [        S[        SSU 0U5       3SS9  M)  [        XU5        M7     g ! [         a  n[        SU SU  S	U 3SS9   S nAM]  S nAff = f)
NÚdk_sendzDealer OS: úpurchase.dk_sendr!  T)Úflushú
DealerKit z for purchase z after the Mac came free: )r~  ÚprintÚapply_purchase_actionr-  rÞ   )rø  rp  r#  ru  s       r(   Ú_dk_waiting_flushr†  ]  sŠ   € Ü+Ö-Ñˆ�Sð	eØ˜Ó"ô ˜Ô$9Ð:LÈtÐUXÈkÐ[^Ó$_Ð#`ÐaÐimÔnä'¨°SÖ9ò .øô ó 	eÜ�J˜v˜h n°S°EÐ9SÐTUÐSVÐWÐ_c×dÐdûð	eús   ’ A´AÁ
A-ÁA(Á(A-c           
      ó®  • [        5       nUR                  U 5      n[        R                  " U5      (       d  gSSSSSSSS	.R	                  X5      n[        XAS
S9nU(       a!  [        R                  " US   SSU SU 3US9  g UR                  SUS   U/S.5      n[        U[        5      (       aP  UR	                  S5      SL a<  [        XU5        [        R                  " US   SSU SUR	                  S5       3US9  g[        R                  " US   SU S3US9  g! [         a*  n[        R                  " US   SSU SU 3US9   SnAgSnAff = f)a  The DealerKit half of a change made here or in Dealer OS, sent at
once (2026-09-07, Steven: "everytime we update anything in dealer os
for it to automatically at that point sync to dealerkit"): a check in,
a chip, a transport fee or a cancelled sale's removal goes to DealerKit
the moment it is applied, on a row job, instead of waiting for the
daily run. Best effort: a busy Mac or a refusal is written on the
car's timeline and the next automatic run picks it up (the dk_*
stamps are still empty), so the change itself always stands.Nzcheck inzprice and fee after the chipztransport feeÚremovalzretail pricerS  zprice and fee)r$  rU  r&  rV  rT  rS  rW  T©rñ  r!  r   rƒ  z not sent: ©r#  Údealerkit_push_rowra   ©r’   ra   r¾   ÚstartedFú waits until the Mac is free: Úreasonz sent to DealerKitz# waits for the next automatic run: )rA  Ú_purchase_rowr   ri  r9   rr  Úlog_purchase_eventÚ_start_run_apir¹   rº   r|  r   )	rø  rp  r#  ÚdoorsrÌ   r¦   Úwhyr?   ru  s	            r(   r-  r-  j  s}  € ô ‹H€EØ
×
Ñ
˜cÓ
"€CÜ�?Š?˜3×ÑØØ"Ð,JÐYhØ ¨NÈÐapñrßruÑruÐv|ó  sFð 
ä
˜c°Ñ
6€CÞ
Ü
×Ò˜c $™i¨¸
À5À'ÈÐUXÐTYÐ6ZÐ`cÒdØðcØ× Ñ Ð)=ÀcÈ%ÁjÐ\bÐ[cÑ!dÓeˆÜ�aœ×Ñ 1§5¡5¨Ó#3°uÒ#<ä˜3¨Ô,Ü×!Ò! # d¡)¨[Ø$.¨u¨gÐ5SÐTU×TYÑTYÐZbÓTcÐSdÐ"eÐknópô ×!Ò! # d¡)¨[¸U¸GÐCUÐ:VÐ\_Ó`øÜó cÜ
×Ò˜c $™i¨Ø *¨5¨'Ð1TÐUVÐTWÐXÐ^a÷	cûðcús   ÂA=D  ÄD  Ä 
EÄ* EÅEc                 óL   • [         R                  " U S5        [        U SU5        g)a  A new Retail est. goes to DealerKit the moment it is set, from the
Mac's own page or from Dealer OS (2026-09-07, Steven: "im more
interested in getting the info over into dealerkit from dealer os more
constantly and quicker/instantly"). Before this it waited for the
automatic run, so a price changed at nine in the morning reached
DealerKit at quarter past four. The stored dk_check's own retail_price
is cleared first so the automatic run still picks this up as the
safety net when the Mac is busy and the row job cannot start.rT  N)r   r,  r-  ©rø  r#  s     r(   Ú_dealerkit_retail_nowr—  ‹  s    € ô ×Ò�c˜8Ô$Ü˜C ¨3Õ/r*   c                 ó   • [        U SU5        g)zWThe DealerKit half of a check in (2026-09-05), now one case of
_dealerkit_followup_now.r$  N)r-  r–  s     r(   r*  r*  ˜  s   € ô ˜C ¨CÕ0r*   c                 ó   • [        U =(       d    0 5      n [        5       R                  U R                  S5      5      nUR                  S5      (       a  [	        US    S35      e[
        R                  " U R                  S5      U R                  S5      5      nUc  [	        SUS    35      eU R                  S5      =(       d    [
        R                  " SU05      n[        UR                  S	5      =(       d    0 5      nUR                  S5      UR                  S5      4X#4:w  d  U R                  S
5      (       aV  UR                  X#SU R                  S
5      =(       d    UR                  S
5      S.5        [        R                  " US   U5        [        R                  " US   X#UR                  S5      SS9nUS    SU SU=(       d    S 3nUR                  S5      b  UR                  S5      OUR                  S5      n[
        R                  " X'U R                  S5      5      nUbE  [        R                  " US   USSUS 3S9  X„S'   [        R                  " US   U5        USUS 3-  n[        5         U$ )aÍ  DealerKit's stage, live (2026-09-09, Steven: "build option one").
One purchase.dk_stage action from Dealer OS, queued off DealerKit's
webhook: the stage goes onto dk_check and the car's Status follows
through db.match_dealerkit_stage, exactly as the timed check does;
on Ordered or Sold DealerKit's price follows into the Retail est.
(the price is DealerKit's alone there). The push goes at once.
Returns a short note; raises ValueError with the refusal reason.r!  rZ  ra   z0 is cancelled, DealerKit's stage is not followedÚstatusrt  zno stage in the action for rh  rg  Úapi_idÚwebhook)rh  rt  Ústage_sourcer›  Údelivery_onÚ	DealerKitrŠ  r†  z on DealerKit, zstatus already matchedÚretail_estimate_overriderR   Úretail_priceu*   Retail est. followed DealerKit's price, Â£Ú,)r#  r�  u   , price followed to Â£)rº   rA  r�  r9   r   r   Ústage_numberÚstage_labelÚupdater   Úset_dealerkit_checkÚmatch_dealerkit_stageÚlocked_price_to_followÚset_retail_estimate_overrider  )	rè  rÌ   rš  rt  rv  r0  ÚnoteÚshownÚfigures	            r(   Úapply_dk_stager­  ž  s<  € ô 	ˆQ�W�"‹€AÜ
‹(×
 Ñ
  §¡ t£Ó
-€CØ
‡w�wˆ~×ÑÜ˜C ™J˜<Ð'WÐXÓYÐYÜ×)Ò)¨!¯%©%°«/¸1¿5¹5À»>ÓJ€FØ�~ÜÐ6°s¸5±z°lÐCÓDÐDØ�E‰E�'‹N×Xœo×9Ò9Ð;NÐPVÐ:WÓX€EÜ	ˆc�g‰g�jÓ!×' RÓ	(€BØ
�‰Ð"Ó# R§V¡V¨G£_Ð5¸&¸ÓHÈAÏEÉEÐRZÏOÉOØ
�	‰	¨ÐPYØŸU™U 8›_×@°·±°xÓ0@ñBô 	Cä
×Ò˜s 5™z¨2Ô.Ü×#Ò# C¨¡I¨v¸b¿f¹fÀ]Ó>SÐYdÑe€DØ�%‰jˆ\˜˜E˜7 /°$×2RÐ:RÐ1SÐT€DØ36·7±7Ð;UÓ3VÑ3bˆC�G‰GÐ.Ô/Ðhk×hoÑhoð  qBó  iC€EÜ×3Ò3°FÀ1Ç5Á5ÈÓCXÓY€FØÑÜ
×'Ò'¨¨D©	°6¸{Ø1[Ð\bÐcdÐ[eÐ/fò	hà#ˆ>ÑÜ
×Ò˜s 5™z¨2Ô.ØÐ(¨°¨
Ð3Ñ3ˆÜÔØ€Kr*   c                 ó6  • [         R                  R                  [        U =(       d    S5      5      R	                  5       n[
        R                  " SSU5      R	                  S5      nU(       d  UnUR                  5       R                  S5      (       d  US-  nU$ )z‚A file name safe to write under data/purchase_docs: letters, digits,
dots, dashes and underscores only, ending .pdf. Pure, tested.r    z[^A-Za-z0-9._-]+Ú-z-.z.pdf)	rï   rð   Úbasenamer  r¼   ÚreÚsubr  rª   )r   ÚfallbackÚbases      r(   Ú_safe_doc_namerµ  Á  st   € ô �7‰7×ÑœC §
¨›OÓ,×2Ñ2Ó4€DÜ�6Š6Ð% s¨DÓ1×7Ñ7¸Ó=€DÞØˆØ�:‰:‹<× Ñ  ×(Ñ(Ø�‰ˆØ€Kr*   c                 ó8  ^• SSK n[        5       nUR                  U R                  S5      5      nU R                  S5      (       a  [	        XpU5      $ U R                  S5      =(       d    SR                  5       nUR                  S5      (       d  [        S5      eUb  Tc  UR                  5       u  nmUR                  U=(       d    SR                  S	5      S	-   5      (       d  [        S
USS  S35      eUc  U4S jnU" U5      n	U	(       a	  U	SS S:w  a  [        S5      e[        R                  R                  [        SSUS   5      n
[        R                  " U
SS9  [        U R                  S5      =(       d    U R                  S5      =(       d    S5      n[        R                  R                  X«5      n[!        US5       nUR#                  U	5        SSS5        [$        R&                  " US   U5        U R                  S5      =(       d    SR                  5       n[$        R(                  " US   SU S3US9  [+        5         [$        R,                  " U5      (       a  [/        US   SU5        U S US    S!3$ U S US    S"3$ ! , (       d  f       N¸= f)#aÉ  purchase.document from Dealer OS (2026-09-16): Dealer OS holds a
document for a bought car (the MotorCheck history check PDF) and says
where to fetch it. Fetched with this Mac's own token, kept under
data/purchase_docs/<reg>/, remembered on the row, and, when the car is
already on DealerKit, sent there now with the documents follow up. A
car not on DealerKit yet carries it with its whole send. `fetch`
(tests) replaces the download and returns the bytes.r   Nr!  ÚwithdrawÚurlr    )zhttp://zhttps://z+purchase.document carries no link to fetch.Ú/z,purchase.document link is not on Dealer OS (ry   z), refused.c                 óÖ   >• SS K nUR                  R                  U ST 3SS.S9nUR                  R                  USS9 nUR	                  5       sS S S 5        $ ! , (       d  f       g = f)Nr   zBearer ÚBidBrain)ÚAuthorizationz
User-Agent)Úheadersry   ©Útimeout)Úurllib.requestÚrequestÚRequestÚurlopenÚread)ÚuÚurllibÚreqÚrespÚapi_keys       €r(   ÚfetchÚ$apply_document_action.<locals>.fetchâ  s[   ø€ Û!Ø—.‘.×(Ñ(¨ÈÐPWÈyÐFYÐisÑ4tÐ(ÐuˆCØ—‘×'Ñ'¨°RÐ'Ñ8¸DØ—y‘y“{÷ 9×8×8ús   Á AÁ
A(r$  s   %PDF-z4Dealer OS did not hand over a PDF for this document.r   Úpurchase_docsra   T©Úexist_okr   ÚkindÚdocumentÚwbr¦   údocument from Dealer OSr   z+ kept, goes to DealerKit with the documentsrŠ  rS  z
 kept for z, sent to DealerKitz, goes with the send)rg   rA  r�  r9   Úwithdraw_documentr¼   Ú
startswithr   Ú_dealer_os_configÚrstriprï   rð   ÚjoinÚHEREÚmakedirsrµ  rÚ   Úwriter   Úadd_dealer_os_documentr‘  r  ri  r-  )rè  r#  Úbase_urlrÉ  rÊ  rg   r“  rÌ   r¸  r   Údoc_dirr   rð   rß   r0  s      `           r(   Úapply_document_actionrÞ  Í  s2  ø€ ó Ü‹H€EØ
×
Ñ
˜aŸe™e D›kÓ
*€CØ‡u�uˆZ×ÑÜ  ¨Ó-Ð-Ø�5‰5�‹<×˜2×
$Ñ
$Ó
&€CØ�>‰>Ð1×2Ñ2ÜÐFÓGÐGØÑ˜7™?Ø%×7Ñ7Ó9Ñˆ�'Ø�>‰>˜8Ÿ> r×1Ñ1°#Ó6¸Ñ<×=Ñ=ÜÐGÈÈCÈRÈÀzÐQ\Ð]Ó^Ð^Ø�}õ	#ñ
 �‹:€DÞ�4˜˜�8˜xÓ'ÜÐOÓPÐPÜ�g‰g�l‰lœ4 ¨¸#¸e¹*ÓE€GÜ‡K‚K� $Ò'Ü˜!Ÿ%™% ›-×F¨1¯5©5°«=×F¸JÓG€DÜ�7‰7�<‰<˜Ó&€DÜ	ˆd�DÔ	˜QØ	�‰�Œ÷ 
ä×Ò˜c $™i¨Ô.Ø�E‰E�'‹N×7Ð7×>Ñ>Ó@€DÜ×Ò˜#˜d™) [°T°FÐ:eÐ2fÐloÒpÜÔÜ	‡‚�s×ÑÜ  D¡	¨;¸Ô<Ø��z # e¡* Ð-@ÐAÐAØˆV�:˜c %™j˜\Ð)=Ð>Ð>÷ 
Õ	ús   ÇJÊ
Jc                 óf  • [        UR                  S5      =(       d    UR                  S5      =(       d    S5      n[        R                  " U S   U5      nU H  n [        R
                  " U5        M     UR                  S5      =(       d    SR                  5       n[        U5      =(       a    [        U R                  S5      5      nU S3nU(       a  US	-  nU(       a#  [        R                  " U S   S
X‚S9  [        5         U(       a  U SU S    S3$ U SU S    S3$ ! [         a     M×  f = f)a:  Dealer OS takes a document back (2026-10-05, S77RUU): the MotorCheck
report was for another car, the private plate having moved on. The
file comes off the row and off this Mac so it never goes to DealerKit.
One already on DealerKit cannot be taken off from here, so that is
said plainly, on the car's timeline too.r   rÏ  rÐ  r!  r¦   rÒ  Údk_documents_pushed_atz3 taken back by Dealer OS, it is for a different carz9. It may already be on DealerKit, remove it there by handr   rŠ  ú (ra   Ú)ú for z( was not kept here, nothing to take back)rµ  r9   r   Úremove_dealer_os_documentrï   rV  ró   r¼   r�   r‘  r  )	rÌ   rè  r#  r   Úgonerð   r0  Úsentrª  s	            r(   rÓ  rÓ  ú  s  € ô ˜!Ÿ%™% ›-×F¨1¯5©5°«=×F¸JÓG€DÜ×'Ò'¨¨D©	°4Ó8€DÛˆð	Ü�IŠI�dŽOñ ð
 �E‰E�'‹N×7Ð7×>Ñ>Ó@€DÜ�‹:×Aœ$˜sŸw™wÐ'?Ó@ÓA€DØˆVÐFÐG€DÞØÐKÑKˆÞÜ
×Ò˜c $™i¨°dÒDÜÔÞ'+ˆdˆV�2�c˜%‘j�\ Ð#Ðs°D°6¸¸sÀ5¹z¸lÐJrÐ1sÐsøô ó 	Úð	ús   ÁD"Ä"
D0Ä/D0c           
      óŠ  • [        5       n[        U=(       d    0 5      nU S:X  a„  UR                  S5      UR                  S5      UR                  S5      S.nUR                  XBS9  [	        UR                  S5      SU5        SUR                  S5       S	UR                  S5       3$ U S
:X  aW  UR                  UR                  S5      UR                  S5      S.US9nSUR                  S5       SUR                  S5       3$ U S:X  ar  UR                  UR                  S5      UR                  S5      UR                  S5      S.USS9  [	        UR                  S5      SU5        SUR                  S5       3$ U S:X  a`  UR                  UR                  S5      UR                  S5      S.US9  [        UR                  S5      U5        SUR                  S5       3$ U S:X  Ga  UR                  UR                  S5      5      n[        U5      nU(       a  [        SR                  U5      5      eUR                  SUS   [        [        R                   5      S.5      n[#        U[        5      (       ab  UR                  S5      S L aN  [%        US   S!U5        [&        R(                  " US   S"S#UR                  S5      =(       d    S$ 3US9  S%US    S&3$ [&        R(                  " US   S"S'US9  S(US    3$ U S):X  aˆ  UR                  UR                  S5      5      n[&        R*                  " U5      (       a  [        US    S*35      eUR-                  SUR                  S5      0US9  S+US    S,UR                  S5       S-3$ U S.:X  aà  [        U5      nUR/                  S/S05      =(       d    S1R1                  5       n	U	(       a  UR3                  S2U	S3.5      S4   US2'   S5 H  n
UR                  U
5      S6;   d  M  S7XJ'   M      S8 H,  n
UR                  U
5      S6;   d  M  UR/                  U
S05        M.     UR5                  XBS9nS9UR                  S5       S:UR                  S5       3$ U S;:X  Gao  UR                  S<5      nUR                  UR                  S5      5      nUS=:X  a  UR                  S>US   S?.5      nOKUS@:X  a  [        SA5      eU[6        ;   a!  UR                  SUS   [6        U   /S.5      nO[        SBU< 35      e[#        U[        5      (       a˜  UR                  S5      S L a„  US=:X  a#  [        UR                  S5      =(       d    SC5      e[%        US   [6        U   U5        [&        R(                  " US   S"SDU SEUR                  S5      =(       d    S$ 3US9  SDU SFUS    S&3$ [&        R(                  " US   S"U SG3US9  SDU SHUS    3$ U SI:X  aq  [9        UR                  SJ5      UR                  SK5      UR                  SL5      UR                  SM5      5        SNUR                  SK5      < SOUR                  SJ5       3$ U SP:X  aQ  [;        UR                  SJ5      UR                  SK5      5        SNUR                  SK5      < SQUR                  SJ5       3$ [        SRU  35      e)Sz¬Step two of the way back (2026-09-03): each kind through the Mac's
own door. Returns a short note; raises ValueError with the refusal
reason, which the sync logs and skips.zpurchase.chipr!  Úamountr�  )r!  rè  r�  rŠ  rU  zchip of z on zpurchase.unchipÚchip_id)r!  ré  z removed from zpurchase.cancelÚreason_typeÚreason_text)r!  rê  rë  r   ©r#  r_   rV  z
cancelled zpurchase.checkinrš  )r!  rš  zchecked in r�  r¸   r‹  ra   rŒ  r�  Fr€  r   z,DealerKit send waits until the Mac is free: zanother job is runningzDealerKit send for z waits until the Mac is freez&sent whole to DealerKit from Dealer OSzDealerKit send started for zpurchase.deleteúQ is on DealerKit, so it cannot be deleted from the list. Cancel the sale instead.zdeleted rá  z) from the purchases listzpurchase.addÚsupplier_newNr    r  )Úcolumnr	  r	  )rã  r&  rÆ  r   )rR   Úbought_dateÚnoteszadded z as purchase zpurchase.dkrp  ÚcheckÚdealerkit_check_row)r’   ra   Údue_inzkThe due in date is no longer sent to DealerKit. Check the car in when it arrives and DealerKit will follow.zunknown DealerKit action z&the Mac is busy, try again in a minuterƒ  rŽ  rã  z! sent to DealerKit from Dealer OSz started for z
views.saverG  r   rÄ  r`   zview z
 saved on zviews.deletez deleted on zunknown purchase action )rA  rº   r9   Ú_record_chipr-  Ú_remove_chipÚ_cancel_purchaseÚ_check_in_purchaser*  r�  rc  r   r×  r’  r  r   Ú
SEND_ITEMSr¹   r|  r   r‘  ri  Ú_delete_manual_purchaserJ  r¼   Ú_add_purchase_dropdown_optionÚ_add_manual_purchaseÚ_DK_ACTIONSrN  rQ  )rÏ  rè  r#  r“  rg  rU  rÌ   Úproblemsr?   Únewr¬   rp  s               r(   r…  r…    s–  € ô ‹H€EÜˆQ�W�"‹€AØˆÓØ—5‘5˜“;¨!¯%©%°«/ÀQÇUÁUÈ8Ã_ÑUˆØ×Ñ˜1ÐÑ&Ü §¡ d£¨V°SÔ9Ø˜!Ÿ%™% ›/Ð*¨$¨q¯u©u°T«{¨mÐ<Ð<ØÐ Ó Ø×!Ñ!¨¯©¨t«ÀÇÁÀyÓAQÑ"RÐX[Ð!Ð\ˆØ˜$Ÿ(™( 8Ó,Ð-¨^¸A¿E¹EÀ$»K¸=ÐIÐIØÐ Ó Ø×Ñ a§e¡e¨D£kÀ!Ç%Á%ÈÓBVØ./¯e©e°MÓ.Bñ DØILÐU`ð 	ñ 	bä §¡ d£¨X°sÔ;Ø˜AŸE™E $›K˜=Ð)Ð)ØÐ!Ó!ð 	× Ñ ¨¯©¨d«¸q¿u¹uÀX»Ñ!OÐUXÐ ÑYÜ §¡ d£¨SÔ1Ø˜QŸU™U 4›[˜MÐ*Ð*ØÐ!Ô!ð ×!Ñ! !§%¡%¨£+Ó.ˆÜ$ SÓ)ˆÞÜ˜SŸX™X hÓ/Ó0Ð0Ø× Ñ Ð)=ÀcÈ%ÁjÜ+/´×0JÑ0JÓ+Kñ"Mó Nˆä�aœ×Ñ 1§5¡5¨Ó#3°uÒ#<ô ˜3˜t™9 i°Ô5Ü×!Ò! # d¡)¨[Ø$PÐQR×QVÑQVÐW_ÓQ`×Q|Ðd|ÐP}Ð"~ð  EHòIà(¨¨U©¨Ð4PÐQÐQÜ
×Ò˜c $™i¨Ð6^ÐdgÒhØ,¨S°©Z¨LÐ9Ð9ØÐ Ó ð ×!Ñ! !§%¡%¨£+Ó.ˆÜ�?Š?˜3×ÑÜ  E¡
˜|ð ,8ð 8ó 9ð 9à×%Ñ% t¨Q¯U©U°4«[Ð&9¸sÐ%ÑCØ˜#˜e™*˜ R¨¯©¨d« }Ð4MÐNÐNØˆ~ÓÜ�‹GˆØ�u‰u�^ TÓ*×0¨b×7Ñ7Ó9ˆÞØ!×?Ñ?È:Ð`cÑ@dÓeÐfmÑnˆAˆj‰MÛ%ˆAØ�u‰u�Q‹x˜:Õ%Ø�“ñ &ó =ˆAØ�u‰u�Q‹x˜:Õ%Ø—‘�a˜–ñ =ð ×&Ñ& qÐ&Ð2ˆØ˜Ÿ™˜e›�~ ]°1·5±5¸³;°-Ð@Ð@Øˆ}ÔØ—‘�x“ˆØ×!Ñ! !§%¡%¨£+Ó.ˆØ�WÓØ×$Ñ$Ð-BÈ3ÈuÉ:Ñ%VÓW‰AØ�xÓô
 ð Jó Kð Kà”{Ó"Ø×$Ñ$Ð-AÈ#ÈeÉ*Ô`kÐlrÑ`sÐ_tÑ%uÓv‰AäÐ8¸¹
ÐCÓDÐDÜ�aœ×Ñ 1§5¡5¨Ó#3°uÒ#<Ø˜Ó Ü  §¡ x£×!\Ð4\Ó]Ð]ô ˜3˜t™9¤k°&Ñ&9¸3Ô?Ü×!Ò! # d¡)¨[Ø$.¨v¨hÐ6TÐUV×UZÑUZÐ[cÓUd÷  VAð  iAð  UBð  #Cð  ILòMà ˜x u¨S°©Z¨LÐ8TÐUÐUÜ
×Ò˜c $™i¨¸¸Ð@aÐ6bÐhkÒlØ˜F˜8 =°°U±°Ð=Ð=Øˆ|ÓÜ˜Ÿ™˜v›¨¯©¨f«°q·u±u¸ZÓ7HÈ!Ï%É%ÐPYÓJZÔ[Ø�q—u‘u˜V“}Ñ' z°!·%±%¸³-°ÐAÐAØˆ~ÓÜ˜1Ÿ5™5 ›=¨!¯%©%°«-Ô8Ø�q—u‘u˜V“}Ñ' |°A·E±E¸&³M°?ÐCÐCÜ
Ð/°¨vÐ6Ó
7Ð7r*   )rQ  rS  rD  rL   rN   r>  r@  rA  r?  rV  rY  rW  rZ  rX  r=  rŠ   rT  rU  rF  )r\  r]  r^  Úbuying_rulesr‹   c           
      óX  • U S:X  a  S[        U5      0$ U S:X  a  S[        U5      0$ U S:X  a  S[        U5      0$ U S:X  a  S[        U5      0$ U S:X  Ga¯  [	        U[
        5      (       a  U(       d  [        S5      e[        [        U5      [        [        5      -
  5      nU(       a  [        SU 35      e[        Ub  UO	[        5       5      nUR                  5        Hû  u  pVUS:X  a¯  [	        U[
        5      (       aš  UR                  U5      =(       d    0 n[        U5      [        U5      -   Vs0 s HY  nU[	        UR                  U5      [
        5      (       a   0 UR                  U5      =(       d    0 EXh   EOUR                  U5      _M[     snXE'   Mº  US	:X  a7  [	        U[
        5      (       a"  0 UR                  U5      =(       d    0 EUEXE'   M÷  XdU'   Mý     [        U5      n	U Vs0 s H  oUU	;   d  M
  XYU   _M     sn$ [        S
U < S[        [        5       35      es  snf s  snf )aN  Turn one settings.set section from Dealer OS into the exact rows the
Mac's own Settings page would save, through the same validators, or
raise ValueError with the reason (2026-09-03, step three). Returns
{settings key: clean value}. Pure apart from reading the current
merged settings for the buying rules merge (passed in by a test).r\  r]  r^  r‹   r   z5buying_rules must be an object with at least one rulez1these are not buying rules Dealer OS may change: r=  rŠ   zunknown settings section ú, expected one of )rc  rd  re  Ú_validate_integrationsr¹   rº   r   r+  rl   ÚBUYING_RULE_KEYSr  r¾   r9   rv  r  ÚSETTINGS_SECTIONS)
Úsectionr	  ÚcurrentÚunknownÚmergedr¬   r­   r´  Úpkr•  s
             r(   Úsettings_section_cleanr  †  s  € ð Ð&Ó&Ø$Ô&BÀ5Ó&IÐJÐJØÐ$Ó$Ø"Ô$>¸uÓ$EÐFÐFØ�/Ó!ØÔ!8¸Ó!?Ð@Ð@Ø�.Ó ØÔ 6°uÓ =Ð>Ð>Ø�.Ô Ü˜%¤×&Ñ&®eÜÐTÓUÐUÜœ˜U›¤cÔ*:Ó&;Ñ;Ó<ˆÞÜÐPÐQXÐPYÐZÓ[Ð[Ü Ñ!4‘gÔ:JÓ:LÓMˆØ—K‘K–M‰DˆAØ�KÓ¤J¨q´$×$7Ñ$7Ø—z‘z !“}×*¨�ä'*¨4£y´3°q³6Ò'9ó;Ú'9 ð  ÄzÐRS×RWÑRWÐXZÓR[Ô]a×GbÑGbÐ"C d§h¡h¨r£l×&8°bÐ"C¸Q¹UÑ"CÐhl×hpÑhpÐqsÓhtÒtÙ'9ñ;�“	àÐ(Ó(¬Z¸¼4×-@Ñ-@Ø: §
¡
¨1£× 3°Ð:¸Ð:�“	à�q“	ñ "ô # 6Ó*ˆÙ%*Ó9¢U °5©j“�˜‘8’¡UÑ9Ð9Ü
Ð0°±Ð;MÌdÔSdÓNeÐMfÐgÓ
hÐhùò;ùò :s   Ä/A H"Ç*	H'Ç7	H'c                 óž   • U S;   a  [        5       R                  X0US9nUS   $ [        X5      n[        R                  " XSS9  [        U5      $ )a  Save one section from Dealer OS exactly as the Mac's own page would:
the purchases page's three sections go through its own save (which
also refuses removing a dropdown answer real cars still carry), the
rest through the Settings page's validators. Returns the keys saved.)r\  r]  r^  ©Ú
updated_atrW   )rA  Ú_purchase_page_settingsr  r   rK  r+  )r  r	  r#  r  r?   r•  s         r(   Úapply_settings_sectionr  ©  sQ   € ð ÐMÓMÜ‹H×,Ñ,¨gÐ-=È*Ð,ÐUˆØ�‰zÐÜ" 7Ó2€EÜ‡O‚O�EÒ1Ü�%‹=Ðr*   c                  óŽ   ^• SSK mS n S nS nS nS nU4S jnSS	 jnS
 nSS jnS n	SS jn
SS jnSS jnS nXxX�XX«XÓXTX,S.$ )zhWhat the sync needs from this process: the watchlist kick for a star,
and run control for a run request.r   Nc                 ó   • [        X5        g rå  )r1  )rg  r#  s     r(   r+  Ú,_dealer_os_hooks.<locals>.set_purchase_field¾  s
   € Ü˜QÕ$r*   c                 ó   • [        XU5      $ rå  )r…  )rÏ  Úpayloadr#  s      r(   Úpurchase_actionÚ)_dealer_os_hooks.<locals>.purchase_actionÁ  s   € Ü$ T°CÓ8Ð8r*   c                 ó   • [        X5      $ rå  )rÞ  )r  r#  s     r(   rÐ  Ú"_dealer_os_hooks.<locals>.documentÄ  s   € Ü$ WÓ2Ð2r*   c                 ó   • [        U 5      $ rå  )r­  )r  s    r(   Údk_stageÚ"_dealer_os_hooks.<locals>.dk_stageÇ  s   € Ü˜gÓ&Ð&r*   c                 óâ  • U =(       d    0 n[         R                  " UR                  S5      =(       d    SUR                  S5      UR                  S5      UR                  S5      5         [        S5      nUR                  S5       S3UR                  S5      (       a  S	-   $ S
UR                  S5      =(       d    S S3-   $ ! [         a!  nUR                  S5       SU S3s S nA$ S nAff = f)Nra   r    r›  r   Úsold_atÚdealerkit_sale_pricesz
: queued, r�  úreading nowzreads on the next quiet pass (r�  rþ  râ  z(: queued, reads on the next quiet pass ()r   Úqueue_sale_price_readr9   Ú
_start_runrÞ   )r  rè  r?   ru  s       r(   Ú
sale_priceÚ$_dealer_os_hooks.<locals>.sale_priceÊ  s  € ð �M�rˆÜ
× Ò  §¡ u£×!3°°Q·U±U¸8³_ÀaÇeÁeÈFÃmÐUV×UZÑUZÐ[dÓUeÔfð	QÜÐ2Ó3ˆAØ—e‘e˜E“l�^ :Ð.À1Ç5Á5È×CSÑCS°-ñ  Xð  XÐ[yÐz{×zÑzð  AIó  {J÷  {Tð  NTð  zUð  UVð  ZWñ  Xð  XøÜó 	QØ—e‘e˜E“l�^Ð#KÈAÈ3ÈaÐPÕPûð	Qús$   Á+7C Â#C Ã
C.ÃC)Ã#C.Ã)C.c                 ó˜   >•  TR                  U R                  S5      5      n[        5         U$ ! [         a  nSU S3n S nAN!S nAff = f)Nra   zcould not move it now (râ  )Úrelease_nowr9   rÞ   r  )r  rª  ru  rg   s      €r(   Úhold_releaseÚ&_dealer_os_hooks.<locals>.hold_releaseÕ  sN   ø€ ð	2Ø×(Ñ(¨¯©°UÓ);Ó<ˆDô 	ÔØˆøô ó 	2Ø,¨Q¨C¨qÐ1�Dûð	2ús   ƒ / ¯
A	¹AÁA	c                 ó   • [        XX#S9$ )Nr  )r  )r  r	  r#  rƒ   s       r(   Úsettings_sectionÚ*_dealer_os_hooks.<locals>.settings_sectionß  s   € Ü% g°cÑIÐIr*   c                 óR   •  [        U 5        [        S5        g ! [         a     g f = f©NÚ	watchlist)r  r"  rÞ   )rg  s    r(   Ú
after_starÚ$_dealer_os_hooks.<locals>.after_starâ  s)   € ð	Ü  Ô#Ü�{Õ#øÜó 	Ùð	úó   ‚ ™
&¥&c                 ó¸  • U=(       d    0 n/ nU S:X  a  U(       a  SSR                  U5      /nOBU S:X  a  U(       a  U Vs/ s H  nSU 3PM
     nnOU S:X  a  UR                  S5      (       a  S/n [        X5      nSU 0UE[        R                  " 5       UR                  S5      S.EqU$ s  snf ! [        [        4 a  nS	[        U5      S
.n SnANWSnAff = f)z A run asked for from Dealer OS. The same body shapes the Run
modal's own /api/start-run takes: platforms for the daily run,
backfill for the Didn't win history.r•   ú--platformsr¢  rC  ú--Ú	lost_bidsÚbackfillú
--backfillF©r�  r�  Nr’   Ú_seq©rƒ   Úseq)r×  r9   r"  ÚKeyErrorr   r  rÝ   Ú_dealer_os_last_command)r’   r=  ÚbodyÚextrarè  r?   ru  s          r(   Ú	start_runÚ#_dealer_os_hooks.<locals>.start_runé  sØ   € ð
 �z�rˆØˆØ�%‹<žIØ" C§H¡H¨YÓ$7Ð8‰EØ�KÓ¦IÙ'0Ó1¢y !�r˜!˜“X¡yˆEÐ1ˆEØ�KÓ D§H¡H¨Z×$8Ñ$8Ø!�NˆEð	5Ü˜3Ó&ˆAð $)¨#Ð"_°Ð"_¼$¿)º)»+ÈdÏhÉhÐW]ÓN^Ò"_ÐØˆùò 2øô
 œ*Ð%ó 	5Ø!¬S°«VÑ4�Aûð	5ús   Á B,Á2B1 Â1CÃCÃCc                  óR   •  [         R                  S 0 5        g ! [         a     g f = frå  )r@  Ú	_stop_runrÞ   rè   r*   r(   Ústop_runÚ"_dealer_os_hooks.<locals>.stop_runý  s)   € ð	Ü×$Ñ$ T¨2Õ.øÜó 	Ùð	úr1  c                 ó|  • [         R                  SSS9   [        R                  S0 5      nS[	        UR                  S5      5      0UESUR                  S5      (       a  SOSUR                  S5       S	30EnSS0UE[        R                  " 5       U S.Eq	U$ ! [         a  nS
[        U5      S.n SnAN=SnAff = f)a~  Update to the latest GitHub Release, asked for from Dealer OS's
Software update card (Steven 2026-09-05): the exact same _update
the Mac's own settings page button runs, refusals and all (a run
in flight, no newer release, a dirty tree), recorded in
last_command so Dealer OS can show the outcome. On success the
server restarts itself a moment later and comes back on the new
code.rœ   N©rƒ   r	  r�  Úupdatedr�  zAlready on the latest version (Úversionz).Fr8  r’   r¥  r:  )
Ú_version_memor¥  r@  Ú_updater�   r9   r   r  rÝ   r=  ©r;  r?   ru  s      r(   Ústart_updateÚ&_dealer_os_hooks.<locals>.start_update  sÊ   € ô 	×Ñ ¨4ÐÑ0ð	5Ü×&Ñ& t¨RÓ0ˆAØœD §¡ yÓ!1Ó2ð q°að qØ 1§5¡5¨×#3Ñ#3™4Ð;ZÐ[\×[`Ñ[`ÐajÓ[kÐZlÐlnÐ9oñqˆAð $)¨(Ð"W°aÐ"W¼t¿yºy»{ÐSVÒ"WÐØˆøô ó 	5Ø!¬S°«VÑ4�Aûð	5ús   –A#B Â
B;Â#B6Â6B;c                 ó   • Sq U qg)z{Dealer OS's Diagnostics card asked for the latest now: the sync
loop sends on its next tick and records it in last_command.TN)Ú_diag_wantedÚ_diag_wanted_seq)r;  s    r(   Úsend_diagnosticsÚ*_dealer_os_hooks.<locals>.send_diagnostics  s   € ð ˆØÑr*   c                 óê   • [         R                   " 5       q [        S5      nUR                  S5      (       a  S$ SUR                  S5      =(       d    S S3$ ! [         a  nSU S3s SnA$ SnAff = f)	zàThe shortlist deep read (Dealer OS v4.1.0, 2026-09-17): read the
starred cars' own pages now if the Mac is free, else on its next
quiet moment (_deep_read_catch_up in the sync loop). Never fatal,
the rows are already queued.Ú	deep_readr�  r   zreads when the Mac is free (r�  rþ  râ  N)rÝ   Ú_deep_read_last_tryr"  r9   rÞ   rL  s      r(   rU  Ú#_dealer_os_hooks.<locals>.deep_read   sv   € ô #Ÿiši›kÐð	7Ü˜;Ó'ˆAØ$%§E¡E¨)×$4Ñ$4�=ÐuÐ<XÐYZ×Y^ÑY^Ð_gÓYh×YrÐlrÐXsÐstÐ:uÐuøÜó 	7Ø1°!°°AÐ6Õ6ûð	7ús"   —"A ºA Á
A2Á"A-Á'A2Á-A2c                 ó   • [        X5        g)zñA Retail est. set from Dealer OS, on to DealerKit at once
(2026-09-07). The sync writes the override itself; this is only
the DealerKit half, so a sync running without this process (the
standalone pull) simply leaves it to the automatic run.N)r—  r–  s     r(   Úretail_changedÚ(_dealer_os_hooks.<locals>.retail_changed-  s   € ô
 	˜cÕ'r*   )r/  r@  rD  r+  r  r*  rM  rR  rY  r  r'  r#  rÐ  rU  rå  )rg   )r+  r  rÐ  r  r#  r'  r*  r/  r@  rD  rM  rR  rU  rY  rg   s                 @r(   Ú_dealer_os_hooksr[  º  s†   ø€ ó ò%ò9ò3ò'ò	QõôJòôò(ôô,ô7ò(ð %È(ð  Rað  Yeð  _mð  Uað  IQñ  jð  jr*   c           	      óR  • SSK nSSKJnJn  [        R
                  " 5       =(       d    0 nUR                  [        R                  R                  [        SS5      U5      n[        R                  " 5       n[        (       dB  U[        :X  a  g[        SL=(       a    US   [        S   :g  nU(       d  U[        -
  [        :  a  gUR!                  [        UR#                  5       U[        R$                  " 5       =(       d    0 [&        [(        S9n	[        n
[*        n UR-                  XU	5        UqUqSqSqU
(       a  S	S
X{S.qgg! UR0                   aT  n[3        USS5      (       a  UqUqSq SnAg[5        SU S35        U
(       a  S	S[7        U5      X{S.qSq SnAg SnAgSnAff = f)zýSend the diagnostics document when there is a reason to: Dealer OS
asked, a run finished (its "at" moved), or a log grew and the last send
was DIAG_EVERY ago or more. Never raises; a failed send is a line in
the server log and the next tick tries again.r   N)Údealer_os_syncÚdiagnosticsr   r   r,   )Ú
started_atFr^  T)r’   r�  rƒ   r;  Úmissing_routez)Dealer OS sync: diagnostics send failed (râ  )r’   r�  r�  rƒ   r;  )rg   Úbidbrainr]  r^  r   rå   Úfingerprintrï   rð   r×  rØ  rÝ   rP  Ú_diag_sent_fpÚ_diag_sent_atÚ
DIAG_EVERYÚcollectÚ_bidbrain_versionr  rÛ   Ú_serve_started_atrQ  Úpost_diagnosticsr=  ÚDealerOsSyncErrorr$   r„  r  )rÜ  rÉ  rg   r]  r^  rÇ   Úfpr„   Úruns_changedÚdocrñ  r;  ru  s                r(   Ú_diagnostics_tickrn  7  s  € ó ß4Ü×"Ò"Ó$×*¨€JØ	×	 Ñ	 ¤§¡§¡¬d°F¸FÓ!CÀZÓ	P€BÜ
�)Š)‹+€CßŠ<Ø”ÓØÜ$¨DÐ0×N°R¸±U¼mÈAÑ>NÑ5NˆÞ ¤mÑ 3´jÓ @ØØ
×
Ñ
œd I×$?Ñ$?Ó$AÀ:Ìr×OaÒOaÓOc×OiÐgiÜ&Ô3Dð ð F€Cä€EÜ
€Cð!Ø×'Ñ'¨¸3Ô?ØˆØˆØˆØÐÞØ.;ÈÐTWÑ&dÑ#ð øà×+Ñ+ó !Ü�1�o u×-Ñ-ð ˆMØˆMØ ˆLÜÜÐ9¸!¸¸AÐ>Ô?ÞØ.;ÈÔY\Ð]^ÓY_ÐgjÑ&wÐ#Ø �Lô ûð!ús   Ä'E ÅF&ÅF!Å/(F!Æ!F&c                 óf   • [        [        S U =(       d    0 R                  5        5       5      5      $ )zWhen every recorded pass last finished, as one comparable value. A
change means a pass ended, whoever started it. Pure, tested.c              3   ó‚   #   • U  H5  u  pU[        U=(       d    0 R                  S 5      =(       d    S5      4v •  M7     g7f)rƒ   r    N)r  r9   r«   s      r(   re   Ú&_finish_fingerprint.<locals>.<genexpr>e  s0   é € ÐcÒHbÁÀ˜œC § b§¡¨dÓ 3× 9°rÓ:Õ;ÒHbùs   ‚=?)rb  r+  r¾   )rÇ   s    r(   Ú_finish_fingerprintrr  b  s)   € ô ”ÑcÈ×IYÐWY×H`ÑH`ÔHbÓcÓcÓdÐdr*   )rƒ   rI   c                  óÈ   • SSK n [        R                  " 5       nU[        S   -
  S:”  a!   U R                  5       [        S'   U[        S'   [        S   $ ! [         a     Nf = f)z]The Mac's timed jobs for the heartbeat, re read from the launchd
files at most once a minute.r   Nrƒ   ry   rI   )rg   rÝ   Ú_schedule_memoÚ_run_schedulerÞ   )rg   r„   s     r(   Ú_schedule_cachedrv  k  sg   € ó Ü
�)Š)‹+€CØ
Œ^˜DÑ!Ñ! BÓ&ð	Ø%.×%<Ñ%<Ó%>ŒN˜6Ñ"ð  #Œ�tÑÜ˜&Ñ!Ð!øô ó 	Ùð	ús   «A Á
A!Á A!c                  óž  • SSK n SSKJn  [        5       n0 SUR	                  5       _SU R                  5       _SUR                  5       _SUR                  S5      _SUR                  S5      _S	UR                  5        VVs0 s H%  u  p4UR                  S
5      (       d  US:X  d  M#  X4_M'     snn_S[        5       (       a
  [        5       OS_S[        R                  " 5       _S[        R                  " 5       =(       d    0 R                  5        VVs0 s H=  u  p4U[        U[         5      (       a   0 UES[#        UR                  S5      5      0EOU_M?     snn_S[        R$                  " 5       =(       d    0 R                  5        VVs0 s H&  u  p4U0 UES[#        UR                  S5      5      0E_M(     snn_SU R'                  5       _SU R)                  5       R                  S5      =(       d    / _SU R+                  5       _S[,        R.                  " 5       _S[0        _S[3        5       _S[5        5       _nU$ s  snnf s  snnf s  snnf )z~The heartbeat body: what the cockpit's own light and Run modal read,
plus the cursor so the reply can say how much is waiting.r   N©r]  rƒ   Úbidbrain_versionÚcursorrí   rÆ   rÅ   r¨   r©   rÈ   r„   rÇ   r¶   r  r   r�   rŠ   r‰   r‹   r   Úlast_commandÚschedulerI  )rg   ra  r]  r÷   Úutcnowrg  rz  r9   r¾   rª   rí  rõ   rÝ   r   rå   r¹   rº   rþ   r  Ú_platforms_enabledÚ_get_settingsÚ_integrationsr�   Úclient_registryr=  rv  Ú_version_block)rg   r]  rš  r¬   r­   rá   s         r(   Ú_dealer_os_snapshotrƒ  y  s_  € ó Ý'Ü‹]€FðØˆn×#Ñ#Ó%ðà˜I×7Ñ7Ó9ðð 	�.×'Ñ'Ó)ðð 	�—‘˜GÓ$ð	ð
 	�F—J‘J˜zÓ*ðð 	 6§<¡<¤>Ô^¢>™4˜1°Q·Z±ZÀ
×5KÑ5KÈqÐT]É~“$�!’$¡>Ò^ðð 	¬×)9Ñ)9”|”~¸tðð 	Œt�yŠy‹{ðð 	Ü$&×$5Ò$5Ó$7×$=¸2×#DÑ#DÔ#FôHÚ#F™4˜1ð ÌjÐYZÔ\`×NaÑNaÐJ˜aÐJ ¬K¸¿¹¸iÓ8HÓ,IÒJÐghÒhÙ#FòHðð 	Ü%'×%7Ò%7Ó%9×%?¸R×$FÑ$FÔ$HôJÚ$H™D˜Að ÐI˜aÐI ¬;°q·u±u¸YÓ7GÓ+HÑIÒIÙ$HòJðð 	˜I×8Ñ8Ó:ðð 	˜i×5Ñ5Ó7×;Ñ;Ð<PÓQ×WÐUWðð 	˜	×/Ñ/Ó1ðð  	”×%Ò%Ó'ð!ð" 	Ô/ð#ð( 	Ô$Ó&ð)ð. 	”>Ó#ð/€Dð2 €Kùó' _ùóHùóJs   Â"H=
Â'H=
ÄAI
Æ-I	
c                  ó.  • [         R                   " 5       [        -
  S:  a  g [        R                  " 5       (       d  g [         R                   " 5       q [        S5        g! [         a     gf = f! [         a  n [        SU  S35         Sn A gSn A ff = f)a  A shortlist deep read asked for while the Mac was busy (a daily run,
a login window) starts on the next quiet moment, at most one try every
30 s. Rows the pass has already answered are not waiting, so a dead
login never loops: it is answered with an error once.é   NrU  z&Shortlist deep read: could not start (râ  )rÝ   rV  r   Úpending_deep_readsrÞ   r"  r„  )ru  s    r(   Ú_deep_read_catch_upr‡  ž  sŒ   € ô ‡y‚yƒ{Ô(Ñ(¨2Ó-ØðÜ×$Ò$×&Ñ&Øð 'ô Ÿ)š)›+Ðð=Ü�;Õøô	 ó Ùðûô
 ó =ÜÐ6°q°c¸Ð;×<Ñ<ûð=ús)   £A! ÁA1 Á!
A.Á-A.Á1
BÁ;BÂBc                 óÀ   • U (       a  U(       d  [        U[        5      (       d  g[        UR                  S5      5      =(       a    [        UR                  S5      5      $ )z¡Whether the Mac should update itself right now: the switch is on,
nothing is running, and the version block says a newer real release
is published. Pure, tested.FÚupdate_availableÚlatest)r¹   rº   r�   r9   )rŒ   rþ  rI  s      r(   Ú_auto_update_duer‹  ²  s@   € ö –d¤*¨W´d×";Ñ";ØÜ�—‘Ð.Ó/Ó0×P´T¸'¿+¹+ÀhÓ:OÓ5PÐPr*   c            	      ó~  • [         R                  " S5          [        S5      (       a´  [        5       (       d¥  [	        5       n [        SSU 5      (       a‰  [        SU R                  S5       SU R                  S5       S	35         [        R                  S
0 5      n[        SUR                  S5      (       a  S[        UR                  S5      5      -   OS S35        [         R                  " S5        MÝ  ! [         a  n[        SU 35         S
nAN5S
nAff = f! [         a  n[        SU 35         S
nANZS
nAff = f)aè  The Mac updates itself (Steven 2026-09-17, "make the Mac update
itself"): every two minutes, with the auto_update switch on and the
Mac idle, the same _update the Software update button runs, refusals
and all (a run in flight, a dirty tree). On success the server
restarts itself a moment later and comes back on the new release.
Off by default; Settings > Connections (or Dealer OS's Automation
switches card) turns it on. Never raises: a failed look is logged and
tried again next time.rv   TÚauto_updateFzAuto update: r  z to rŠ  z, installing now.NrH  zrestarting on rI  znothing to dor¡   zAuto update: not now, zAuto update thread: rÇ  )rÝ   ÚsleepÚ_integration_enabledrí  r‚  r‹  r„  r9   r@  rK  r  r   rÞ   )r­   r?   ru  s      r(   Ú_auto_update_loopr�  »  s%  € ô 	‡J‚Jˆr„NØ
ð	.Ü# M×2Ñ2¼>×;KÑ;KÜ"Ó$�Ü# D¨%°×3Ñ3Ü˜M¨!¯%©%°	Ó*:Ð);¸4ÀÇÁÀhÃÐ?PÐPaÐbÔcð<Ü*×2Ñ2°4¸Ó<˜Ü ÐZ[×Z_ÑZ_Ð`i×ZjÑZjÐ.>ÄÀQÇUÁUÈ9ÓEUÓAVÒ.VÐpð  .Að  ABð  Cô  Dô
 	�
Š
�3Œñ øô &ó <ÜÐ 6°q°cÐ:×;Ñ;ûð<ûäó 	.ÜÐ(¨¨Ð,×-Ñ-ûð	.ús=   ™A+D ÂAC5 Ã5
DÃ?DÄD ÄDÄD Ä
D<Ä$D7Ä7D<c                  óÚ  • SS K n SSKJn   U R                  5       (       a  [	        S5        SnS nSnS n  U R                  S5      (       d  [        R                  " S5        M0  U R                  5       u  pgU(       a  U(       d  [        R                  " S	5        Mh  [        5       nU(       a  U(       d  [        5         [        5         Un [        [        R                  " 5       5      n	Ub  X•:w  a  [!        5         [        5         U	nSn
 UR#                  Xg[%        5       5      n['        UR)                  S
5      =(       d    S5      n
USLa  [        R*                  " SSS5        SnU
S:”  d  [        R                  " 5       U-
  S:”  a…  [        R                  " 5       nU R3                  S[5        5       S9nU(       aQ  UR)                  S5      (       a;  S U l        [!        5         [        5       (       d   U R9                  5         [        5         [        5       (       d
  [;        5         [<        (       a9  U(       d2  [        R                  " 5       [>        -
  S:¼  a  Sq U RA                  SS9   [C        Xg5        [        R                  " S5        GMZ  ! [
         a     GNpf = f! [
         a    Un	 GNÆf = f! UR,                   av  n[/        USS5      (       a1  Ub  [	        SU S35        S n[        R                  " S5         S nAGMÒ  USLa#  [        R*                  " SS[1        U5      5        Sn S nAGNÎS nAff = f! [
         a  n[	        SU S35         S nAGNXS nAff = f! [
         a  n[	        SU S35         S nAGNS nAff = f! [
         a  n[	        SU S35         S nAGN3S nAff = f! [
         a/  n[	        SU 35        [        R                  " S5         S nAGNYS nAff = f)Nr   rx  zMRemoved the old Motorway CSV marker from data/ (it lives under data/logs now)rœ   FTÚdealer_os_pushr…  ry   Úpendingr   r    r`  zDealer OS sync: z Waiting for it to be updated.rw   é,  )Ú
standaloneÚhooksÚappliedz4Dealer OS sync: could not rebuild the cockpit page (râ  rS   )r•  z Dealer OS sync: re push failed (z)Dealer OS sync: diagnostics tick failed (r$  zDealer OS sync thread: )"rg   ra  r]  Úclear_old_csv_markerr„  rÞ   r�  rÝ   rŽ  rÕ  rí  r  r†  rr  r   rå   ré   Úpost_statusrƒ  r‚   r9   Úrecord_site_healthrj  r$   r  Údealer_os_pull_passr[  Ú_settings_cacheÚrender_onlyr‡  r	  r
  Údealer_os_push_passrn  )rg   r]  Ú	last_pullÚlast_health_okÚwas_busyÚlast_fprÜ  rÉ  rþ  rk  r“  Úreplyru  Úgots                 r(   Ú_dealer_os_sync_loopr¥  Õ  si  € ÛÝ'ðØ×)Ñ)×+Ñ+ÜÐaÔbð €IØ€NØ€HØ€GØ
ðU	Ø×1Ñ1Ð2B×CÑCÜ—
’
˜2”ÙØ )× ;Ñ ;Ó =ÑˆHÞ¦7Ü—
’
˜2”ÙÜ!Ó#ˆDÞ¦ô &Ô'ä!Ô#ØˆHðÜ(¬×):Ò):Ó)<Ó=�ð Ñ" r£}Ü!Ô#Ü%Ô'ØˆGØˆGð+Ø&×2Ñ2°8ÔFYÓF[Ó\�Ü˜eŸi™i¨	Ó2×7°aÓ8�Ø!¨Ò-Ü×)Ò)¨+°t¸RÔ@Ø%)�Nð ˜‹{œdŸiši›k¨IÑ5¸Ó;Ü ŸIšI›K�	Ø×3Ñ3¸uÔL\ÓL^Ð3Ð_�Þ˜3Ÿ7™7 9×-Ñ-Ø04�IÔ-Ü%Ô'Ü)×+Ñ+ð_Ø%×1Ñ1Ô3ô *Ô+Ü!×#Ñ#Ü#Ô%ßÒ®´·²³Ô?RÑ1RÐVWÓ1WØ#(Ð ðCØ×1Ñ1¸UÐ1ÑCðHÜ! (Ô4ô �JŠJ�qŒMòg øô ó ÚðûôD ó Ø“ðûð "×3Ñ3ó +Ü˜1˜o¨u×5Ñ5ð &Ñ1ÜÐ 0°°Ð3QÐRÔSØ%)�NÜ—J’J˜s”OÞØ!¨Ò.Ü×)Ò)¨+°u¼cÀ!»fÔEØ%*�Nÿùð+ûô,  )ó _Ü!Ð$XÐYZÐX[Ð[\Ð"]×^Ò^ûð_ûô !ó CÜÐ<¸Q¸C¸qÐA×BÒBûðCûô ó HÜÐAÀ!ÀÀAÐF×GÒGûðHûô ó 	ÜÐ+¨A¨3Ð/Ô0Ü�JŠJ�r�NŠNûð	ús	  Œ J ¶,N1 Á$6N1 Â.N1 ÃJ  Ã) N1 Ä
AJ3 Å&BN1 Ç3L< ÈAN1 ÉM# É+N
 É6N1 Ê
JÊJÊ J0Ê,N1 Ê/J0Ê0N1 Ê3L9Ë<L4Ë?N1 Ì(L4Ì.N1 Ì4L9Ì9N1 Ì<
M ÍMÍN1 ÍM Í N1 Í#
NÍ-NÍ<N1 ÎNÎN1 Î

N.ÎN)Î#N1 Î)N.Î.N1 Î1
O*Î;$O%Ï%O*r$  c                  óÀ  • SSK n Sn  [        R                  " S5        U R                  S5      (       d  M0  [        R                  " 5       U-
  [        S-  :  a  MU  [        5       (       a  Mf  [        S5      nUR                  S5      (       a  [        R                  " 5       nM�  ! [         a.  n[        SU 35        [        R                  " S	5         SnAN5SnAff = f)
aõ  DealerKit's stages, read every DEALERKIT_STAGE_MINUTES while the Mac
is idle (2026-09-07, Steven: "if something updates on dealer os or
vice versa then they both need to know straight away"). DealerKit
tells nobody when a car moves, so the Mac looks; the quiet stage check
(daily_run --dealerkit-stages) opens no window and reads one record
per live car. Off whenever the DealerKit purchases connection is off,
so a Mac without DealerKit set up never runs it. A busy Mac is tried
again a minute later.r   Nrœ   ry   Údealerkit_purchasesÚdealerkit_stagesr�  zDealerKit stage loop: rÇ  )
rg   rÝ   rŽ  r�  ÚDEALERKIT_STAGE_MINUTESrí  r"  r9   rÞ   r„  )rg   rö   r?   ru  s       r(   Ú_dealerkit_stage_looprª  @  sµ   € ó Ø€DØ
ð	Ü�JŠJ�rŒNØ×1Ñ1Ð2G×HÑHÙÜ�yŠy‹{˜TÑ!Ô$;¸bÑ$@Ó@ÙÜ×ÑÙÜÐ-Ó.ˆAØ�u‰u�Y×ÑÜ—y’y“{�ñ øô ó 	ÜÐ*¨1¨#Ð.Ô/Ü�JŠJ�s�O‰Oûð	ús(   ‰,B% ·#B% ÁB% Á-6B% Â%
CÂ/$CÃCc           
      ó„  • [         R                  U    nUR                  (       a2  [        UR                  5      (       d  [	        UR
                   S35      e[           [        5       (       aß  [         Vs/ s H  n[        U5      (       d  M  UPM     nn[        X5      nUc  SS[        5       S.sSSS5        $ U H  n[        [        U   5        M     U H  n [        U   R                  SS9  M     [        5       (       a  SS[        5       S.sSSS5        $ [        UR
                   SS	R!                  S
 U 5       5       S35        ["        R$                  " ["        R&                  R)                  [*        5      SS9  [-        [*        S5      n[/        ["        R0                  5      nUR2                  (       aE  U(       a>   [5        UR7                  S5      5      [5        UR7                  S5      5      p©U	 SU
 3US'   UR:                  (       d  [=        UR>                  5        [@        RB                  " [D        RF                  /URH                  QUQ[J        U[@        RL                  SUS9[        U '   SSS.sSSS5        $ s  snf ! [         a     GMÀ  f = f! [8        [        4 a     N¬f = f! , (       d  f       g= f)a‹  The one way a pass is started. Checks the registry's gate (a run
switched off in Settings is refused with the reason), the shared busy
lock (refused with what is in the way), primes the progress file with
the run's own "starting" line, then spawns the registry's command with
any extra arguments, logging to the manual run log. Returns the same
{started, running, reason} shape every page reads.z) is turned off in Settings > Connections.NFT)r�  r©   r�  é   r¾  z
: stopped ú, c              3   ó\   #   • U  H"  n[         R                  U   R                  v •  M$     g 7frå  )r�   rŽ   r¦   rë  s     r(   re   Ú_start_run.<locals>.<genexpr>v  s    é € Ð3VÒQUÈA´E·J±J¸q±M×4GÖ4GÒQUùs   ‚*,z to start straight awayrÍ  ÚabÚxÚyr¢  ÚBIDBRAIN_GLASS_WIN)ÚcwdÚstdoutÚstderrÚstart_new_sessionÚenv)r�  r©   )'r�   rŽ   r�   r�  r   r¦   Ú	_run_lockrí  ræ  r¨   ró  rõ   rü  ÚwaitrÞ   r„  r×  rï   rÙ  rð   ÚdirnameÚ
MANUAL_LOGrÚ   rº   ÚenvironÚwinr‚   r9   r~   rï  rà   r¯   Ú
subprocessÚPopenÚsysÚ
executableÚargvrØ  ÚSTDOUT)r’   r?  rg  r•   r¬   r©   rò  Úlogfr¸  r±  r²  s              r(   r"  r"  \  s  € ô �*‰*�S‰/€CØ
‡x‡xÔ,¨S¯X©X×6Ñ6Ü˜CŸI™I˜;Ð&OÐPÓQÐQÞ	Ü×ÑÝ"(Ó8¢&˜Q¬H°Q¯K—q¡&ˆGÐ8Ü$ SÓ2ˆDØ‰|Ø#(°TÄ\Ã^ÑT÷ 
‰ó �Üœ6 !™9Ö%ñ ã�ðÜ˜1‘I—N‘N¨2�NÓ.ñ ô
 ×ÑØ#(°TÄ\Ã^ÑT÷ 
‰ô  �S—Y‘Y�K˜z¨$¯)©)Ñ3VÑQUÓ3VÓ*VÐ)WÐWnÐoÔpÜ
�Š”B—G‘G—O‘O¤JÓ/¸$Ò?Ü”J Ó%ˆÜ”2—:‘:ÓˆØ�7�7–qðÜ˜1Ÿ5™5 ›:“¬¨A¯E©E°#«J«�1Ø/0¨c°°1°#¨J�Ð(Ñ)ð �y�yÜ˜CŸL™LÔ)Ü ×&Ò&Ü�^‰^Ð/˜cŸh™hÐ/¨Ð/´TØ¤
× 1Ñ 1ÀTÈsñTŒˆs‰ð  ¨DÑ1÷C 
‰ùâ8øô !ó Ûðûô œzÐ*ó Ùðú÷5 
�úsz   ÁJ1Á5JÂJÂJ1Â: J1ÃJ	Ã1 J1ÄB(J1Ç=JÈA9J1ÊJ1Ê	
JÊJ1ÊJÊJ1ÊJ.Ê+J1Ê-J.Ê.J1Ê1
J?c                  óÎ   • [         R                  " S5      n U (       a  U $ [        R                  R	                  S5      n[        R                  R                  U5      (       a  U$ S$ )aH  Where the gh CLI actually is. Not always on launchd's own minimal
PATH (the same gap already found and worked around setting up the
real GitHub Releases, 2026-08-21: gh was downloaded straight to
~/.local/bin/gh rather than installed system wide), so PATH is
checked first, that fallback second, never guessed further than
that.Úghz~/.local/bin/ghN)ÚshutilÚwhichrï   rð   Ú
expanduserÚexists)Úfoundr³  s     r(   Ú_gh_pathrÍ  Š  sL   € ô �LŠL˜Ó€EÞØˆÜ�w‰w×!Ñ!Ð"3Ó4€HÜ—w‘w—~‘~ h×/Ñ/ˆ8Ð9°TÐ9r*   c                 ó<  • U =(       d    SR                  5        Vs/ s H)  oR                  5       (       d  M  UR                  5       PM+     nnU(       d  gU Vs/ s H#  n[        R                  " SU5      (       d  M!  UPM%     nnU=(       d    US   $ s  snf s  snf )aÃ  The real release tag out of the tags pointing at one commit. Every
release this project has ever cut is named with the v ("v3.14.138",
the Release workflow's own convention), so a v tag always wins over
one without it. Pure, so it can be checked on its own.

Why it has to choose (2026-09-20): a release was cut by hand with the
version typed without its v, which made a SECOND tag on the same
commit, "3.14.138" beside "v3.14.138". git lists the tags on a commit
in name order, so the bare one came first and the Mac read its own
version as "3.14.138" while the newest release said "v3.14.138". They
never match, so the Software update card would have offered an update
that was already installed, over and over, and a Mac with auto update
on would have restarted itself every couple of minutes for as long as
that tag existed. The stray tag could not be deleted from here, and a
duplicate could always happen again, so the Mac stops being fooled by
one instead.r    Nz^v\dr   )rù   r¼   r±  r5  )ÚtagsÚtÚnamesÚ	versioneds       r(   Ú_release_tagrÓ  ˜  sw   € ð" "&§¨× 7Ñ 7Ô 9ÓGÒ 9˜1¿W¹W¿Y‹YˆQ�W‰WŽYÑ 9€EÐGÞØÙ!Ó:šE�q¤R§X¢X¨g°q×%9—™E€IÐ:Ø×˜ Ñ"Ð"ùò	 Hùò ;s   œB¸BÁ BÁ>Bc                  ó¼  •  [         R                  " / SQ[        SSSS9R                  R	                  5       n U (       a  [        U 5      $ [         R                  " / SQ[        SSSS9R                  R	                  5       n[         R                  " / SQ[        SSSS9R                  R	                  5       nU(       a  U(       a  U SU S3$ S	$ ! [         a     g	f = f)
a³  The tag HEAD is exactly on (a real released version, the normal
case for an installed copy per SETUP_FOR_NEW_USER.md's own "checkout
a tag, not main" instruction), or the branch name and short commit
when it is not (working on main mid session, like this one). Never
guesses, a git failure is reported as an error string, never a blank
or invented version. With more than one tag on the commit,
_release_tag picks the real release one.)ÚgitÚtagz--points-atÚHEADTé
   ©r´  Úcapture_outputrú   r¿  )rÕ  ú	rev-parsez--abbrev-refr×  )rÕ  rÛ  z--shortr×  rá  râ  r  )r¿  r•   rØ  rµ  r¼   rÓ  rÞ   )rÖ  ÚbranchÚshas      r(   Ú_current_versionrÞ  °  sÈ   € ðÜ�nŠnÒBÌØ,0°tÀRñIßIOÉ×PUÑPUÓPWð 	æÜ Ó$Ð$Ü—’Ò LÔRVØ/3¸$ÈñLßLRÉF×SXÑSXÓSZð 	ä�nŠnÒDÌ$Ø,0°tÀRñIßIOÉ×PUÑPUÓPWð 	æ&,¶�&�˜˜C˜5 Ð"ÐC¸)ÐCøÜó Ùðús   ‚AC Á
BC ÃC Ã
CÃCc                  ó|  • [        5       n U (       d  g [        R                  " U SSSS/[        SSSS9nUR                  S	:w  a%  S
UR
                  =(       d    SR                  5       4$ UR                  =(       d    SR                  5       nU=(       d    S
S
4$ ! [         a  nS
[        U5      4s S
nA$ S
nAff = f)a�  The newest real GitHub Release tag (a release, not git tag -l, since
a tag can exist locally without ever having been pushed or turned into
a release, see the REAL VERSIONED RELEASES convention in CLAUDE.md).
Asked through the REST route (gh api .../releases/latest) rather than
gh release list, which goes through GraphQL and shares the account's
GraphQL allowance with everything else that account does; the live
card read "API rate limit already exceeded" on 2026-09-05 the first
time it was pressed. Returns (tag_or_None, error_or_None), never
raises, so a network hiccup or a missing gh reports cleanly rather
than crashing the whole version check.)Nz gh is not installed on this Mac.Úapiz$repos/{owner}/{repo}/releases/latestz--jqz	.tag_nameTrH  rÙ  r   Nzgh api releases/latest failedr    )
rÍ  r¿  r•   rØ  Ú
returncoder¶  r¼   rµ  rÞ   r  )rÇ  r?   rÖ  ru  s       r(   Ú_latest_versionrâ  Æ  sª   € ô 
‹€BÞØ7ðÜ�NŠN˜B Ð'MÈvÐWbÐcÜ#°D¸tÈRñQˆà�<‰<˜1ÓØ˜!Ÿ(™(×EÐ&E×LÑLÓNÐNÐNØ�x‰x�~˜2×$Ñ$Ó&ˆØ—�t˜dÐ"Ð"øÜó Ø”S˜“Vˆ|Õûðús$   ”AB Á*/B Â
B;Â$B6Â0B;Â6B;rG  c            
      óÐ  • [         R                   " 5       n [        S   nU(       a*  U [        S   -
  UR                  S5      (       d  SOS:  a  U$ [        5       n[	        5       u  p4U(       a"  U(       a  UR                  S5      (       a  US   nX#U[        U5      =(       a    X2:g  [         R                  " S[         R                  " U 5      5      S.n[        R                  XS	9  U$ )
aD  The heartbeat's version block: current, latest, whether an update
is waiting, and when that was last looked up. gh release list takes a
network round trip, so the answer is kept for 30 minutes rather than
asked on every 15 second heartbeat; an update or a restart clears it
because the process itself restarts. Never raises.r	  rƒ   Úerrorr”  rÇ  rŠ  z%Y-%m-%dT%H:%M:%SZ)r  rŠ  rä  r‰  Ú
checked_atrG  )	rÝ   rJ  r9   rÞ  râ  r�   ÚstrftimeÚgmtimer¥  )r„   Úmemor  rŠ  Úerrr	  s         r(   r‚  r‚  â  s¾   € ô �)Š)‹+€CÜ˜Ñ!€Dö �”m DÑ)Ñ)¸¿¹À×8IÑ8I©SÈsÓSØˆÜÓ €GÜ!Ó#�K€FÞ
Žt˜Ÿ™ ×*Ñ*à�h‘ˆØ¸CÜ!% f£×!C°&Ñ2CÜŸ=š=Ð)=¼t¿{º{È3Ó?OÓPñR€Eô ×Ñ˜CÐÑ-Ø€Lr*   c                 ób  • U (       a  [         R                  " U 5      O[         R                  " 5       n0 [        EUEn0 [        S   EUR                  S5      =(       d    0 EUS'   0 [        EUR                  S5      =(       d    0 EUS'   0 [
        EUR                  S5      =(       d    0 EUS'   UR                  S5      =(       d    /  Vs/ s H  nU[        ;   d  M  UPM     snUS'   U (       a  [         R                  " U 5      O[         R                  " 5       US'   U (       a  [         R                  " U 5      O[         R                  " 5       US'   S0 S.UR                  S	5      =(       d    0 EUS	'   U (       a  [         R                  " U 5      O[         R                  " 5       US
'   U$ s  snf )a  The buying rule settings, always carrying every real default even
for a platform saved before it existed. A plain top level
{**DEFAULT_SETTINGS, **saved} was found live 2026-08-24 (adding
DealerWay, the fifth platform) to silently drop a brand new platform's
own defaults entirely: the database's own saved "platforms" value (an
older, real edit of Mark's, from before DealerWay existed) replaced
DEFAULT_SETTINGS["platforms"] wholesale rather than merging into it, so
the settings page had nothing to show for the new tab and saving from
any tab failed outright (_validate_settings requires every known
platform key to be present). Merged per platform instead, the exact
same fix already used for "integrations" below, extended to cover
"platforms" too now that the same class of bug has bitten twice. path
is only ever passed by a test wanting an isolated database, db.py's
own functions default their path argument at definition time so a
caller cannot rely on monkeypatching db.DB_PATH after the module has
already loaded.r=  r‹   rŠ   r‰   r\  r]  F)r¢  r£  r^  r_  )
r   rU   r	   r9   r   r   Ú_HIDEABLE_SITESr(  Úget_purchase_columnsÚget_fee_bands)rð   rW   r  rÑ   s       r(   r  r  ú  sŠ  € ö" &*ŒB�OŠO˜DÔ!¬r¯ªÓ/@€EØ,Ô"Ð, eÐ,€HØ_Ô/°Ñ<Ð_ÀÇÁÈ;ÓAW×A]Ð[]Ð_€Hˆ[ÑØ\Ô"6Ð\¸5¿9¹9À^Ó;T×;ZÐXZÐ\€Hˆ^ÑØ#iÔ&?Ð#iÀEÇIÁIÐN`ÓDa×DgÐegÐ#i€HÐÑ ð
 38·)±)Ð<PÓ2Q×2WÐUWÐ2Wó &@Ò2W¨AØ*+¬Ñ*>÷ '(Ñ2Wñ &@€HÐ!Ñ"ö
 JN¤b×&?Ò&?ÀÔ&EÜ+-×+DÒ+DÓ+Fð Ð!Ñ"æEI¤B×$;Ò$;¸DÔ$AÜ)+×)@Ò)@Ó)Bð ÐÑ à05Àbñ !GØ$)§I¡I¨oÓ$>×$DÀ"ð!G€Hˆ_Ñæ6:œB×,Ò,¨TÔ2Ä×@PÒ@PÓ@R€Hˆ[ÑØ€Oùò&@s   ÃF,ÃF,c                 óÂ   • [         R                  " 5       R                  S5      =(       d    0 n[        0 [        EUER                  U 0 5      R                  SS5      5      $ )aH  Whether one automation is switched on, straight from the saved
settings (never cached, unlike daily_run.py's own per process
_get_settings, since this handler lives as long as the server does).
Checked again here, not just relied on the cockpit hiding the button,
so a disabled automation can never run via a direct POST either.r‹   rŒ   T)r   rU   r9   r�   r   )r’   rW   s     r(   r�  r�  #	  sQ   € ô �OŠOÓ×!Ñ! .Ó1×7°R€EÜÐ1Ô'Ð1¨5Ð1×5Ñ5°c¸2Ó>×BÑBÀ9ÈdÓSÓTÐTr*   c                 óH  • [        U [        5      (       d  [        S5      e[        U 5      [        [        5      -
  nU(       a  [        S[        U5       S35      e0 n[         H;  n[        U R                  U5      [        5      (       d  [        U S35      eX   X#'   M=     U$ )a   Turn the raw JSON from each platform tab's own 'Read this platform'
toggle into a clean {key: bool} dict, or raise ValueError. Every known
platform key must be present, an unknown key rejected rather than
silently accepted, the same reasoning as _validate_integrations. Flat
booleans, not nested {"enabled": bool} rows, there is nothing else to a
platform's enabled state the way an integration can carry more later.z&Platform enabled settings are missing.z)Unknown platform(s) in platform_enabled: r¡   ú's enabled state is missing.)r¹   rº   r   rl   r   r+  r9   r�   )rg  r  r  r’   s       r(   r`  r`  -	  s•   € ô �aœ×ÑÜÐAÓBÐBÜ�!‹f”sœ=Ó)Ñ)€GÞÜÐDÄVÈGÃ_ÐDUÐUVÐWÓXÐXØ
€CßˆÜ˜!Ÿ%™% ›*¤d×+Ñ+Ü ˜uÐ$@ÐAÓBÐBØ‘6ˆ‹ñ ð €Jr*   c                 óÔ  • U [         ;  a   [        SU < S[        [         5       S35      eU S:X  a  [        S5      eUR                  S5      =(       d    /  Vs/ s H  o3[         ;   d  M  UPM     nnU(       a  X;  a  UR	                  U 5        U(       d  U Vs/ s H  o3U :w  d  M
  UPM     nnSU0nU [
        ;   a/  0 [        EUR                  S5      =(       d    0 EnU(       + X`'   XeS'   [        R                   Vs/ s H  owS   U :X  d  M  US	   PM     nnU(       a:  0 [        EUR                  S
5      =(       d    0 En	U H  n
SU(       + 0Xš'   M     X•S
'   U$ s  snf s  snf s  snf )aþ  The settings updates for hiding or showing one connection on the
Integrations tab (Steven 2026-08-26: "there is no way to hide the ones
we dont use... if we hide them how do we get them back if we do want
to start using them?"). Pure, so it is testable without a server: takes
the current saved settings dict, returns only the keys to write.

Hiding is deliberately more than cosmetic: a hidden connection's own
real off switches flip with it (a platform's "Read this platform"
toggle, an integration's own enabled state), so "hidden" genuinely
means "we don't use this" everywhere the existing switches are already
respected (the scheduled run, Run now, every gated API endpoint),
with no new enforcement mechanism needed. Showing it again flips them
back on, matching Steven's own framing of unhiding ("if we do want to
start using them"). Cazana alone cannot be hidden: it is the pricing
source every run depends on and the one site with no off switch
anywhere, so a hidden-but-still-running Cazana would just be a lie.úUnknown site r  r¡   r   z0Cazana cannot be hidden, every run prices on it.r‰   rŠ   r‘   r’   r‹   rŒ   )
rë  r   r+  r9   r  r   r   r   Ú_INTEGRATIONSr   )r‘   r”   rW   rÑ   ÚcurÚupdatesÚpeÚiÚ
integ_keysr–   r’   s              r(   Ú_apply_hiderù  A	  sd  € ð" ”?Ó"Ü˜=¨©Ð0BÄ6Ì/ÓCZÐB[Ð[\Ð]Ó^Ð^ØˆxÓÜÐKÓLÐLØ—y‘yÐ!5Ó6×<¸"Ð<Ó
VÒ<�ÄoÑAU�1Ñ<€CÐ
VÞ�$“/Ø�
‰
�4ÔÞÙÓ+š#�Q d¡�q™#ˆÐ+Ø# SÐ)€GØŒ}ÓØSÔ)ÐS¨e¯i©iÐ8JÓ.K×.QÈrÐSˆØ”:ˆ‰Ø&(Ð"Ñ#ô %+×$8Ò$8ÓNÒ$8˜q¸f¹IÈÑ<M“(�!�E”(Ñ$8€JÐNÞØMÔ'ÐM¨E¯I©I°nÓ,E×,KÈÐMˆÛˆCØ#¨¤ZÐ0ˆE‹Jñ à"'�ÑØ€Nùò' Wùò ,ùò Os$   ÁEÁ)EÂ	E Â&E Ã=E%Ä	E%c                 óš  • [        U [        5      (       d  [        S5      e[        U 5      [        [        5      -
  nU(       a  [        S[        U5       S35      e0 n[         Hd  nU R                  U5      n[        U[        5      (       a$  [        UR                  S5      [        5      (       d  [        U S35      eSUS   0X#'   Mf     U$ )a˜  Turn the raw JSON from the settings page's Integrations tab into a
clean {key: {"enabled": bool}} dict, or raise ValueError with a clear
reason. Every known key must be present (the page always sends its
whole checkbox set, same as the buying rule settings do); an unknown
key is rejected rather than silently accepted, so a typo in a future
edit here is caught immediately rather than quietly doing nothing.z'Integration settings must be an object.zUnknown integration(s): r¡   rŒ   rð  )r¹   rº   r   rl   r   r+  r9   r�   )rg  r  r  r’   rÌ   s        r(   r  r  l	  s¶   € ô �aœ×ÑÜÐBÓCÐCÜ�!‹f”sÔ/Ó0Ñ0€GÞÜÐ3´F¸7³OÐ3DÀAÐFÓGÐGØ
€Cß#ˆØ�e‰e�C‹jˆÜ˜#œt×$Ñ$¬J°s·w±w¸yÓ7IÌ4×,PÑ,PÜ ˜uÐ$@ÐAÓBÐBØ˜s 9™~Ð.ˆ‹ñ	 $ð
 €Jr*   c                   óÈ  ^ • \ rS rSrSrU 4S jrS rU 4S jrS rS r	S r
S	 rS
 rS rS rS7S jrS7S jrS8S jrS7S jrS7S jrS7S jrS rS7S jrS7S jrS rS rS rS rS rS rS rS r\ " \!RD                  5      \ " \!RF                  5      -  r$S r%S r&S  r'S! r(S" r)S# r*S$ r+S% r,S& r-S' r.S( r/S) r0S* r1S+ r2S, r3S- r4S. r5S/ r6S0 r7S1 r8S2 r9S3 r:S4 r;S5 r<S6r=U =r>$ )9r@  i�	  zEServes files as normal, plus a small JSON API for the page's actions.c                 óŽ   >• U R                  SS5        U R                  SS5        U R                  SS5        [        TU ]	  5         g )NzCache-Controlz#no-cache, no-store, must-revalidateÚPragmazno-cacheÚExpiresÚ0)Úsend_headerÚsuperÚend_headers)r6  Ú	__class__s    €r(   r  ÚCockpitHandler.end_headers„	  sA   ø€ ð
 	×Ñ˜Ð*OÔPØ×Ñ˜ :Ô.Ø×Ñ˜ CÔ(Ü‰ÑÕr*   c                 ó2  • [         R                  " U5      R                  S5      nU R                  U5        U R	                  SS5        U R	                  S[        [        U5      5      5        U R                  5         U R                  R                  U5        g )NrÖ   úContent-Typezapplication/jsonúContent-Length)
ri   rÊ  ÚencodeÚsend_responser   r  r=   r  ÚwfilerÚ  )r6  ÚcodeÚobjr  s       r(   Ú_jsonÚCockpitHandler._jsonŽ	  sq   € Ü—*’*˜S“/×(Ñ(¨Ó1ˆØ×Ñ˜4Ô Ø×Ñ˜Ð);Ô<Ø×ÑÐ)¬3¬s°7«|Ó+<Ô=Ø×ÑÔØ�
‰
×Ñ˜Õ!r*   c                 ó  >• U R                   S:X  a;   U R                  SS[        R                  " SS9[        R                  " SS9S.5      $ U R                   S	:X  a  U R                  SS
S0[        5       E5      $ U R                   S:X  a  U R                  SS
S0[        5       E5      $ U R                   R                  S5      (       a«  SSK
JnJn  U" U" U R                   5      R                  5      R                  S5      =(       d    S/S   n U R                  SS[        [        R                   " [        R"                  " U5      5      5      [        [        R"                  " U5      5      S.5      $ U R                   R                  S5      (       ay  SSK
JnJn  U" U" U R                   5      R                  5      R                  S5      =(       d    S/S   n U R                  SS[        R$                  " ['        U5      5      S.5      $ U R                   S:X  a#   U R                  SS[)        5       [*        S.5      $ U R                   S:X  aA  [-        5       n[/        5       u  pxU R                  SSXgU[1        U5      =(       a    Xv:g  S.5      $ U R                   S:X  a)   U R                  SS[        R2                  " 5       S.5      $ U R                   S:X  a   U R                  SS[5        5       S.5      $ U R                   S:X  a   U R                  SS[7        5       S.5      $ U R                   S:X  a)   U R                  SS[        R8                  " 5       S .5      $ U R                   S!:X  a"   [;        5       u  pšU R                  SSXšS".5      $ U R                   R=                  S#S$5      S   nUS%:X  a–  [>        R@                  " 5       RC                  S&5      nU RE                  S5        U RG                  S'S(5        U RG                  S)[        [I        U5      5      5        U RK                  5         U RL                  RO                  U5      $ US*:X  a–  [>        RP                  " 5       RC                  S&5      nU RE                  S5        U RG                  S'S(5        U RG                  S)[        [I        U5      5      5        U RK                  5         U RL                  RO                  U5      $ US+:X  a–  [>        RR                  " 5       RC                  S&5      nU RE                  S5        U RG                  S'S(5        U RG                  S)[        [I        U5      5      5        U RK                  5         U RL                  RO                  U5      $ US,:X  a–  [>        RT                  " 5       RC                  S&5      nU RE                  S5        U RG                  S'S(5        U RG                  S)[        [I        U5      5      5        U RK                  5         U RL                  RO                  U5      $ [V        TU ]±  5       $ ! [
         a(  nU R                  SS[        U5      S.5      s S nA$ S nAff = f! [
         a(  nU R                  SS[        U5      S.5      s S nA$ S nAff = f! [
         a(  nU R                  SS[        U5      S.5      s S nA$ S nAff = f! [
         a(  nU R                  SS[        U5      S.5      s S nA$ S nAff = f! [
         a(  nU R                  SS[        U5      S.5      s S nA$ S nAff = f! [
         a(  nU R                  SS[        U5      S.5      s S nA$ S nAff = f! [
         a(  nU R                  SS[        U5      S.5      s S nA$ S nAff = f! [
         a(  nU R                  SS[        U5      S.5      s S nA$ S nAff = f! [
         a(  nU R                  SS[        U5      S.5      s S nA$ S nAff = f)-Nz/api/hiddenéÈ   TF)Úonly_new)rµ   r”   Úreleasedé�  ©rµ   rä  z/api/run-statusrµ   z/api/run-summaryz	/api/bidsr   )ÚurlparseÚparse_qsrq   r    )rµ   ÚregsÚstarsz/api/purchase-eventsr!  )rµ   Úeventsr  )rµ   r  Úbanned_engine_namesz/api/version)rµ   r  rŠ  rä  r‰  z
/api/views)rµ   rM  z/api/purchases)rµ   rC  z/api/lost-bids)rµ   r5  z/api/under-offer)rµ   Úunder_offerz/api/notices)rµ   rr   rt   Ú?r,   z/settings.htmlrÖ   r  ztext/html; charset=utf-8r  z/purchases.htmlz/lost-bids.htmlz/under-offer.html)-rð   r  r   Úhidden_notesÚreleased_notesrÞ   r  r÷   r  rÔ  Úurllib.parser  r  Úqueryr9   r+  Úbidset_regsrk   Úpurchase_eventsr‚   r  ÚBANNED_ENGINE_NAMESrÞ  râ  r�   Úlist_saved_viewsrJ   rX   Úlist_under_offerru   r:   r   Úrender_settingsr  r	  r   r=   r  r
  rÚ  Úrender_purchasesÚrender_lost_bidsÚrender_under_offerr  Údo_GET)r6  ru  r  r  rq   rø  r  rŠ  ré  rr   rt   Ú	path_onlyÚhtmlr  s                €r(   r*  ÚCockpitHandler.do_GET–	  s  ø€ Ø�9‰9˜Ó%ðGØ—z‘z #¨d¼b¿oºoÐW\Ñ>]Ü46×4EÒ4EÈuÑ4Uñ(Wó Xð Xð �9‰9Ð)Ó)Ø—:‘:˜c D¨$Ð#@´+³-Ð#@ÓAÐAØ�9‰9Ð*Ó*Ø—:‘:˜c D¨$Ð#A´,³.Ð#AÓBÐBØ�9‰9×Ñ ×,Ñ,÷ 8Ù!¡(¨4¯9©9Ó"5×";Ñ";Ó<×@Ñ@ÀÓM×UÐRTÐQUÐWXÑYˆIðGØ—z‘z #¨d¼FÄ2Ç>Â>ÔRT×R]ÒR]Ð^gÓRhÓCiÓ<jÔu{Ô|~÷  }Hò  }Hð  IRó  }Só  vTñ  (Uó  Vð  Vð �9‰9×ÑÐ 6×7Ñ7÷ 8Ù™H T§Y¡YÓ/×5Ñ5Ó6×:Ñ:¸4Ó@×HÀRÀDÈ!ÑLˆCðGØ—z‘z #¨dÜ24×2DÒ2DÄSÈÃXÓ2Nñ(Pó Qð Qð �9‰9˜Ó'ðGØ—z‘z #¨dÔ@PÓ@RÜ?Rñ(Tó Uð Uð �9‰9˜Ó&Ü&Ó(ˆGÜ)Ó+‰KˆFØ—:‘:˜cØ wÈ3Ü$(¨£L×$F°VÑ5Fñ$ó ð ð �9‰9˜Ó$ðGØ—z‘z #¨d¼R×=PÒ=PÓ=RÑ'SÓTÐTð �9‰9Ð(Ó(ðGØ—z‘z #¨dÔAUÓAWÑ'XÓYÐYð �9‰9Ð(Ó(ðGØ—z‘z #¨dÔAUÓAWÑ'XÓYÐYð �9‰9Ð*Ó*ðGØ—z‘z #¨dÄ2×CVÒCVÓCXÑ'YÓZÐZð �9‰9˜Ó&ðGÜ'/£zÑ$�Ø—z‘z #¨d¸wÑ'cÓdÐdð —I‘I—O‘O C¨Ó+¨AÑ.ˆ	ØÐ(Ó(ô ×)Ò)Ó+×2Ñ2°7Ó;ˆDØ×Ñ˜sÔ#Ø×Ñ˜^Ð-GÔHØ×ÑÐ-¬s´3°t³9«~Ô>Ø×ÑÔØ—:‘:×#Ñ# DÓ)Ð)ØÐ)Ó)Ü×*Ò*Ó,×3Ñ3°GÓ<ˆDØ×Ñ˜sÔ#Ø×Ñ˜^Ð-GÔHØ×ÑÐ-¬s´3°t³9«~Ô>Ø×ÑÔØ—:‘:×#Ñ# DÓ)Ð)ØÐ)Ó)Ü×*Ò*Ó,×3Ñ3°GÓ<ˆDØ×Ñ˜sÔ#Ø×Ñ˜^Ð-GÔHØ×ÑÐ-¬s´3°t³9«~Ô>Ø×ÑÔØ—:‘:×#Ñ# DÓ)Ð)ØÐ+Ó+Ü×,Ò,Ó.×5Ñ5°gÓ>ˆDØ×Ñ˜sÔ#Ø×Ñ˜^Ð-GÔHØ×ÑÐ-¬s´3°t³9«~Ô>Ø×ÑÔØ—:‘:×#Ñ# DÓ)Ð)Ü‰w‰~ÓÐøô_ ó GØ—z‘z #¨e¼cÀ!»fÑ'EÓFÕFûðGûô ó GØ—z‘z #¨e¼cÀ!»fÑ'EÓFÕFûðGûô ó GØ—z‘z #¨e¼cÀ!»fÑ'EÓFÕFûðGûô ó GØ—z‘z #¨e¼cÀ!»fÑ'EÓFÕFûðGûô ó GØ—z‘z #¨e¼cÀ!»fÑ'EÓFÕFûðGûô
 ó GØ—z‘z #¨e¼cÀ!»fÑ'EÓFÕFûðGûô
 ó GØ—z‘z #¨e¼cÀ!»fÑ'EÓFÕFûðGûô
 ó GØ—z‘z #¨e¼cÀ!»fÑ'EÓFÕFûðGûô" ó GØ—z‘z #¨e¼cÀ!»fÑ'EÓFÕFûðGús  “9X. ÄA#Y# Ç1Z È![ Ê"'\ Ë\7 Ì	], Ì7'^! Í0 _ Ø.
Y Ø8YÙY ÙY Ù#
ZÙ-ZÚ
ZÚZÚ
[
Ú"[Ú?[
Û[
Û
[?Û[:Û4[?Û:[?Ü
\4Ü\/Ü)\4Ü/\4Ü7
])Ý]$Ý])Ý$])Ý,
^Ý6^Þ^Þ^Þ!
_Þ+_ß_ß_ß
`ß `ß=`à`c           
      óh  • 0 SU R                   _SU R                  _SU R                  _SU R                  _SU R                  _SU R
                  _SU R                  _SU R                  _S	U R                  _S
U R                  _SU R                  _SU R                  _SU R                  _SU R                  _SU R                  _SU R                  _SU R                   _0 SU R"                  _SU R$                  _SU R&                  _SU R(                  _SU R*                  _SU R,                  _SU R.                  _SU R0                  _SU R2                  _SU R4                  _SU R6                  _SU R8                  _SU R:                  _SU R<                  _S U R>                  _S!U R@                  _S"U RB                  _EU RD                  U RF                  U RH                  U RJ                  U RL                  U RN                  U RP                  U RR                  S#.EnURU                  U RV                  5      nU(       d  U RY                  S$S%S&S'.5      $  [[        U R\                  RU                  S(S)5      =(       d    S)5      nU(       a  U R^                  Ra                  U5      OS*n[b        Rd                  " U=(       d    S*5      nU" U5      nU RV                  [f        ;   a
  [i        5         U RY                  S+S,S-0UE5      $ ! [j         a(  nU RY                  S.S%[m        U5      S'.5      s S nA$ S nAff = f)/Nz	/api/hidez/api/unhidez/api/start-runz/api/bidz/api/runz/api/run-glassz/api/stop-runz/api/run-clickdealerz/api/run-dealerkitz/api/run-dealerkit-purchasesz/api/push-dealerkitz/api/check-dealerkitz/api/check-all-dealerkitz/api/check-purchasez/api/run-lecapital-fundingz/api/run-purchasesz/api/run-lost-bidsz/api/sync-compmatchz/api/push-dealer-osz/api/reloginz/api/relogin-donez/api/updater  r  z/api/hide-connectionz/api/save-viewz/api/star-viewz/api/delete-viewr  r  z/api/sync-watchlistz/api/run-under-offerr  r  )r  r  r  r  r  r  r  r  i”  Fzunknown endpointr  r  r   s   {}r  rµ   Tr  )7Ú_hideÚ_unhider’  Ú_bidÚ_runÚ
_run_glassrC  Ú_run_clickdealerÚ_run_dealerkitÚ_run_dealerkit_purchasesÚ_push_dealerkit_oneÚ_check_dealerkit_oneÚ_check_all_dealerkitÚ_check_purchase_oneÚ_run_lecapital_fundingÚ_run_purchasesÚ_run_lost_bidsÚ_sync_compmatchÚ_push_dealer_osÚ_reloginÚ_relogin_donerK  Ú	_settingsÚ_integration_settingsÚ_hide_connectionÚ
_save_viewÚ
_star_viewÚ_delete_viewÚ_set_retail_estimateÚ_set_collection_dateÚ_sync_watchlistÚ_run_under_offerÚ_set_purchase_fieldÚ_add_purchase_commentrõ  r÷  rú  rø  rü  rû  r  Ú_lost_bids_page_settingsr9   rð   r  r‚   r½  ÚrfilerÄ  ri   rË  Ú_DEALER_OS_DIRTY_ROUTESr  rÞ   r  )r6  ÚroutesÚfnÚlengthr>  r   r  ru  s           r(   Údo_POSTÚCockpitHandler.do_POST
  s£  € ð$Q�+˜tŸz™zð $Q¨=¸$¿,¹,ð $QØ" D×$7Ñ$7ð$Qà˜dŸi™ið$Qà)3°T·Y±Yð$Qð # D§O¡Oð$Qð 6EÀdÇnÁnð$Qð )¨$×*?Ñ*?ð	$Qð
 '¨×(;Ñ(;ð$Qð 1°$×2OÑ2Oð$Qð (¨×)AÑ)Að$Qð )¨$×*CÑ*Cð$Qð -¨d×.GÑ.Gð$Qð (¨×)AÑ)Að$Qð /°×0KÑ0Kð$Qð '¨×(;Ñ(;ð$Qð '¨×(;Ñ(;ñ$Qð (¨×)=Ñ)=ð$Qð (¨×)=Ñ)=ð$Qð  ! $§-¡-ð!$Qð  2EÀd×FXÑFXð!$Qð"   §¡ð#$Qð$ " 4§>¡>ð%$Qð& .¨t×/IÑ/Ið'$Qð( )¨$×*?Ñ*?ð)$Qð* # D§O¡Oð+$Qð* 6FÀtÇÁð+$Qð, % d×&7Ñ&7ð-$Qð. -¨d×.GÑ.Gð/$Qð0 -¨d×.GÑ.Gð1$Qð2 (¨×)=Ñ)=ð3$Qð4 )¨$×*?Ñ*?ð5$Qð6 ,¨T×-EÑ-Eð7$Qð8 .¨t×/IÑ/Iñ9$Qð: '+×&7Ñ&7Ø*.×*?Ñ*?Ø15×1MÑ1MØ,0×,CÑ,CØ.2×.GÑ.GØ7;×7YÑ7YØ15×1MÑ1MØ26×2OÑ2OòI$QˆðJ �Z‰Z˜Ÿ	™	Ó"ˆÞØ—:‘:˜c¨%Ð:LÑ#MÓNÐNð		CÜ˜Ÿ™×)Ñ)Ð*:¸AÓ>×CÀ!ÓDˆFÞ.4�4—:‘:—?‘? 6Ô*¸%ˆDÜ—:’:˜dŸm eÓ,ˆDÙ�T“(ˆCØ�y‰yÔ3Ó3Ü%Ô'Ø—:‘:˜c D¨$Ð#6°#Ð#6Ó7Ð7øÜó 	CØ—:‘:˜c¨%¼#¸a»&Ñ#AÓBÕBûð	Cús   ÉB,K? Ë?
L1Ì	L,Ì&L1Ì,L1c           
      ó  • [         R                  " UR                  SS5      UR                  SS5      UR                  SS5      UR                  SS5      UR                  SS5      5        [        5         SUR                  SS5      0$ )Nra   r    r�  r2   r3   r   )r   Úhide_carr9   r  ©r6  rg  s     r(   r/  ÚCockpitHandler._hide@
  sl   € Ü
�Š�A—E‘E˜% Ó$ a§e¡e¨H°bÓ&9¸1¿5¹5ÀÈÓ;LØ—E‘E˜' 2Ó&¨¯©¨f°bÓ(9ô	;äÔØ�q—u‘u˜U BÓ'Ð(Ð(r*   c                 óF  • UR                  S5      nU(       d  [        S5      eUR                  S5      n[        [        SS5      =(       d    SnUS;   a  [        R
                  " USUS	9  O [        R
                  " U[        US
5      US	9  [        X$5        S[        5       0$ )aÁ  A manual Retail est. override, set from the purchases page row
(Mark 2026-08-26, "add the ability to edit it from the row if we
want to price it manually lower"). value is None (or missing) to
clear the override, reverting to the capped or plain formula
figure; otherwise it must be a positive number. Returns the whole
refreshed purchases list, same shape as GET /api/purchases, so the
page can repaint from one response rather than a second fetch.r!  úid is requiredr	  r   r    ÚYourÆ  NrŠ  úRetail est.rC  )	r9   r   r$   r%   r   r©  r  r—  rJ   ©r6  rg  rø  r	  r#  s        r(   rH  Ú#CockpitHandler._set_retail_estimateF
  s‹   € ð �e‰e�D‹kˆÞÜÐ-Ó.Ð.Ø—‘�g“ˆÜ”m ]°BÓ7×@¸5ˆØ�JÓÜ×+Ò+¨C°¸3Ó?ä×+Ò+¨C´°e¸]Ó1KÐQTÒUÜ˜cÔ'ØÔ1Ó3Ð4Ð4r*   c                 ó²  • UR                  S5      nU(       d  [        S5      eUR                  S5      n[        [        SS5      =(       d    SnUS;   a  [        R
                  " USUS	9  OH [        R                  R                  [        U5      5        [        R
                  " U[        U5      US	9  S[        5       0$ ! [         a    [        S
5      ef = f)aª  A manual Due in date override, set from the purchases page row
(Mark 2026-08-27, "make the due in editable like the retail est").
value is None (or missing) to clear the override, reverting to
whatever the platform itself last read; otherwise a real calendar
date, "YYYY-MM-DD". Returns the whole refreshed purchases list,
same shape as GET /api/purchases, so the page can repaint from
one response rather than a second fetch.r!  r[  r	  r   r    r\  rÆ  NrŠ  z,Due in date must be a real date, YYYY-MM-DD.rC  )r9   r   r$   r%   r   Úset_collection_date_overrider|   Údater}   r  rJ   r^  s        r(   rI  Ú#CockpitHandler._set_collection_dateZ
  s¼   € ð �e‰e�D‹kˆÞÜÐ-Ó.Ð.Ø—‘�g“ˆÜ”m ]°BÓ7×@¸5ˆØ�JÓÜ×+Ò+¨C°¸3Ó?ðQÜ—‘×+Ñ+¬C°«JÔ7ô ×+Ò+¨C´°U³ÀÒEØÔ1Ó3Ð4Ð4øô ó QÜ Ð!OÓPÐPðQús   Á-(C  Ã Cc                 ó|   ^• [        U4S j[        R                  " 5        5       S 5      nUc  [        ST 35      eU$ )Nc              3   ó<   >#   • U  H  oS    T:X  d  M  Uv •  M     g7f)r!  Nrè   )rb   r?   rø  s     €r(   re   Ú/CockpitHandler._purchase_row.<locals>.<genexpr>{
  s   øé € ÐEÒ2˜!¸±gÀ±n—A‘AÒ2ùs   ƒ“	zno purchase with id )Únextr   rG   r   )r6  rø  rÌ   s    ` r(   r�  ÚCockpitHandler._purchase_rowz
  s;   ø€ ÜÔEœr×0Ò0Ô2ÓEÀtÓLˆØ‰;ÜÐ3°C°5Ð9Ó:Ð:Øˆ
r*   c                 ód   • [        [        SS5      =(       d    Sn[        X5        S[        5       0$ )z¹One inline cell or record row edit from the Mac's own page.
The checks live in apply_purchase_field so the Dealer OS way back
(a purchase.set action) goes through exactly the same door.r   r    r\  rC  )r$   r%   r1  rJ   )r6  rg  r#  s      r(   rL  Ú"CockpitHandler._set_purchase_field€
  s.   € ô ”m ]°BÓ7×@¸5ˆÜ˜QÔ$ØÔ1Ó3Ð4Ð4r*   c                 óô   • UR                  S5      nU(       d  [        S5      e[        [        SS5      =(       d    Sn[        R
                  " X!R                  S5      US9  S[        R                  " U5      0$ )	Nr!  r[  r   r    r\  rú   rŠ  r  )r9   r   r$   r%   r   Úadd_purchase_commentr"  )r6  rg  rø  r#  s       r(   rM  Ú$CockpitHandler._add_purchase_commentˆ
  s_   € Ø�e‰e�D‹kˆÞÜÐ-Ó.Ð.Ü”m ]°BÓ7×@¸5ˆÜ
×Ò §U¡U¨6£]¸Ò<Øœ"×,Ò,¨SÓ1Ð2Ð2r*   c           	      óÚ  • UR                  S5      nU(       d  [        S5      e[        UR                  S5      S5      nU R                  U5      nUR                  S5      (       a  [        S5      eS=pg[        R
                  " 5       R                  S5      =(       d    0 nUR                  S	5      =(       d    0 R                  US
   S5      n	U	(       a•  UR                  S5      bƒ  UR                  S5      bq  [        R                  " US
   US   U-
  5      n
U
bN  X¥S   :w  aF  [        U
5      nUR                  S5      nUb  [        XµS   U-
  S-  -
  S5      nO[        US-  S5      nU=(       d    [        [        SS5      =(       d    Sn[        R                  " X4UR                  S5      =(       d    SR                  5       X&US9  [        R                  " US5        S[        5       0$ )a­  Record a chip. The fee re band is computed HERE, once, at chip
time, and frozen onto the chip row (db.record_chip), never
recomputed later against a table that may have changed. Motorway
re bands (Right Drive is on Motorway Pay and Instant Billing),
Carwow never does (Steven 2026-08-27, matched by FE68CKV's own
real invoice behaviour), both per platform settings; a platform
with no published table gets no computed fee at all.r!  r[  rè  zChip amountrZ  z'this sale is cancelled, nothing to chipNr^  r£  r   Fr_  r`  Úeffective_vatgš™™™™™É?rS   r   r    r\  r�  )r�  r#  Únew_feeÚnew_vatrU  rC  )r9   r   r  r�  r   rU   Úfee_forr  rV   r$   r%   Úrecord_chipr¼   r,  rJ   )r6  rg  r#  rø  rè  rÌ   rp  rq  Úflowr¥  Ú	candidateÚold_vats               r(   rõ  ÚCockpitHandler._record_chip�
  s§  € ð �e‰e�D‹kˆÞÜÐ-Ó.Ð.Ü�a—e‘e˜H“o }Ó5ˆØ× Ñ  Ó%ˆØ�7‰7�>×"Ñ"ÜÐFÓGÐGØ Ð ˆÜ�ŠÓ ×$Ñ$ _Ó5×;¸ˆØ—8‘8˜MÓ*×0¨b×5Ñ5°c¸*±oÀuÓMˆÞ�s—w‘w˜{Ó+Ñ7¸C¿G¹GÀOÓ<TÑ<`ÜŸ
š
 3 z¡?°C¸Ñ4DÀvÑ4MÓNˆIØÑ$¨¸/Ñ6JÓ)JÜ 	Ó*�ØŸ'™' /Ó2�ØÑ&Ü# G°?Ñ/CÀgÑ/MÐQTÑ.TÑ$TÐVWÓX‘Gä# G¨c¡M°1Ó5�GØ×G”Wœ]¨M¸2Ó>×GÀ%ˆÜ
�Š�s¨A¯E©E°(«O×,A¸r×+HÑ+HÓ+JØ¸ò	Bô
 	×Ò˜#˜vÔ&ØÔ1Ó3Ð4Ð4r*   c                 ó:  • UR                  S5      nU(       d  [        S5      e [        UR                  S5      5      nU R	                  U5      nUR                  S5      (       a  [        S5      eU=(       d    [        [        SS5      =(       d    S	n[        R                  " X4US
9n[        R                  " US5        [        R                  " U5      (       a  [        R                  " US5        [        5         U$ ! [        [        4 a    [        S5      ef = f)zÞA chip recorded by mistake comes off again (2026-09-16). Nothing
goes to DealerKit from here: the price and fee stamps are cleared
so the DealerKit tab shows the price as a change to send, and a
person sends it when ready.r!  r[  ré  zchip_id is requiredrZ  z)this sale is cancelled, nothing to changer   r    r\  rŠ  rU  rR  )r9   r   r‚   r~   r�  r$   r%   r   Úremove_chipr,  ri  r  )r6  rg  r#  rø  ré  rÌ   rU  s          r(   rö  ÚCockpitHandler._remove_chip´
  sé   € ð
 �e‰e�D‹kˆÞÜÐ-Ó.Ð.ð	4Ü˜!Ÿ%™% 	Ó*Ó+ˆGð × Ñ  Ó%ˆØ�7‰7�>×"Ñ"ÜÐHÓIÐIØ×G”Wœ]¨M¸2Ó>×GÀ%ˆÜ�~Š~˜c°Ñ4ˆÜ
×Ò˜#˜vÔ&Ü�?Š?˜3×ÑÜ×Ò˜c 7Ô+ÜÔØˆøô œ:Ð&ó 	4ÜÐ2Ó3Ð3ð	4ús   ¥C> Ã>Dc                 ó’  • UR                  S5      nU(       d  [        S5      eUR                  S5      =(       d    SR                  5       n[        R                  " 5       R                  S/ 5       Vs1 s H  ofS   iM	     nnXW;  a  [        S5      eUR                  S5      =(       d    SR                  5       nU(       d  [        S	5      eU R                  U5      n	U	R                  S
5      (       a  [        S5      eU=(       d    [        [        SS5      =(       d    Sn[        R                  " XEX‚US9  S[        5       0$ s  snf )Nr!  r[  rê  r    Úcancelled_reasonr	  z/pick one of the configured cancellation reasonsrë  z3a reason is required, it is kept for the proof packrZ  zthis sale is already cancelledr   r\  rì  rC  )
r9   r   r¼   r   r(  r�  r$   r%   Úcancel_purchaserJ   )
r6  rg  r#  r_   rø  rê  r˜  r/  rë  rÌ   s
             r(   r÷  ÚCockpitHandler._cancel_purchaseË
  s  € Ø�e‰e�D‹kˆÞÜÐ-Ó.Ð.Ø—u‘u˜]Ó+×1¨r×8Ñ8Ó:ˆä×,Ò,Ó.×2Ñ2Ð3EÀrÔJóLÚJð #$�W”:ÙJð 	ð LàÓ%ÜÐNÓOÐOØ—u‘u˜]Ó+×1¨r×8Ñ8Ó:ˆÞÜÐRÓSÐSØ× Ñ  Ó%ˆØ�7‰7�>×"Ñ"ÜÐ=Ó>Ð>Ø×G”Wœ]¨M¸2Ó>×GÀ%ˆÜ
×Ò˜3¨[È&ÒQØÔ1Ó3Ð4Ð4ùòLs   Á3Ec                 óH  • UR                  S5      nU(       d  [        S5      eU R                  U5      n[        R                  " U5      (       a  [        US    S35      eU=(       d    [        [        SS5      =(       d    Sn[        R                  " X2S9  S	[        5       0$ )
zïRemoves a hand added car for good (2026-08-31). Validation
(manual rows only) lives in db.delete_manual_purchase, which
raises a clear ValueError for anything else. A car on DealerKit
is refused here too (2026-09-05): cancel those instead.r!  r[  ra   rí  r   r    r\  rŠ  rC  )	r9   r   r�  r   ri  r$   r%   Údelete_manual_purchaserJ   )r6  rg  r#  rø  rÌ   s        r(   rú  Ú&CockpitHandler._delete_manual_purchaseÞ
  s“   € ð
 �e‰e�D‹kˆÞÜÐ-Ó.Ð.Ø× Ñ  Ó%ˆÜ�?Š?˜3×ÑÜ  E¡
˜|ð ,8ð 8ó 9ð 9à×G”Wœ]¨M¸2Ó>×GÀ%ˆÜ
×!Ò! #Ò/ØÔ1Ó3Ð4Ð4r*   c                 óž  • UR                  S5      nU(       d  [        S5      eU R                  U5      nUR                  S5      (       a  [        S5      eU=(       d    [        [        SS5      =(       d    SnUR                  S5      =(       d    S	nU[
        R                  " 5       ;  a  S	n[
        R                  " X2US
9  S[        5       0$ )Nr!  r[  rZ  z+this sale is cancelled, nothing to check inr   r    r\  rš  zIn stock)r#  rš  rC  )	r9   r   r�  r$   r%   r   Úfinished_statusesÚcheck_in_purchaserJ   )r6  rg  r#  rø  rÌ   rš  s         r(   rø  Ú!CockpitHandler._check_in_purchaseî
  s¨   € Ø�e‰e�D‹kˆÞÜÐ-Ó.Ð.Ø× Ñ  Ó%ˆØ�7‰7�>×"Ñ"ÜÐJÓKÐKØ×G”Wœ]¨M¸2Ó>×GÀ%ˆØ—‘�x“×. JˆØœ×-Ò-Ó/Ó/ØˆFÜ
×Ò˜S°&Ò9ØÔ1Ó3Ð4Ð4r*   c                 óò  • 0 nSU;   aÇ  [        US   5      US'   1 Skn[        R                  " 5       n US   R                  5        Hu  u  pgXd;  a  M  U Vs1 s H  oˆS   iM	     n	nUR	                  SU SU SU SU 35      R                  5       n
U
 H$  nUS   U	;  d  M  [        US   < S	US
    S35      e   Mw     UR                  5         SU;   a  [        US   5      US'   SU;   a  [        US   5      US'   SU;   a  [        US   5      US'   SU;   a  [        US   5      US'   U(       d  [        S5      e[        R                  " X2S9  S[        U5      0$ s  snf ! UR                  5         f = f)aj  A lightweight save for the purchases page's own configuration
(the Fields menu today, a dropdown editor tomorrow): accepts any
subset of the four purchase settings keys and validates only what
was actually sent. The full /api/settings endpoint requires the
whole buying rules payload (platforms and all), which this page
never holds, so it cannot use that route.r\  >   r  r  r  rŒ  r  r  r  r	  zSELECT z$ v, COUNT(*) c FROM purchases WHERE z IS NOT NULL AND z!='' GROUP BY r­   z is still on ÚczF car(s). Retire it instead of removing it, or change those cars first.r]  r^  r_  rD  únothing recognisable to saver  rW   )rc  r   Úconnectr¾   ÚexecuteÚfetchallr   Úcloserd  re  rf  rÑ  rK  r+  )r6  rg  r  r  ÚdirectÚconnr“  r”  r˜  rP  rI   r?   s               r(   r  Ú&CockpitHandler._purchase_page_settingsü
  sº  € ð ˆØ 1Ó$Ü(DÀQÐG[ÑE\Ó(]ˆCÐ$Ñ%òMˆFä—:’:“<ˆDðØ$'Ð(<Ñ$=×$CÑ$CÖ$E‘L�CØÓ(Ù Ù07Ó8²¨1˜gœJ±�DÐ8ØŸ<™<Ø! # ð '!Ø!$ Ð%6°s°e¸>È#ÈðPóQçQYÑQYÓQ[ð ó "˜Ø˜S™6¨Õ-Ü",Ø#$ S¡6¡*¨M¸!¸C¹&¸ð BNð !Nó#Oð Oó "ñ %Fð —
‘
”Ø Ó"Ü&@ÀÐCUÑAVÓ&WˆCÐ"Ñ#Ø˜aÓÜ#:¸1¸_Ñ;MÓ#NˆC�Ñ Ø˜!ÓÜ2°1°[±>ÓBˆC�ÑØ˜qÓ Ü$<¸QÐ?OÑ=PÓ$QˆCÐ Ñ!ÞÜÐ;Ó<Ð<Ü
�Š˜Ò3Øœ ›Ð%Ð%ùò- 9øð —
‘
•ús#   ´#E$ ÁEÁ%9E$ Â"E$ ÅE$ Å$E6c                 óx   • SU;  a  [        S5      e[        US   5      n[        R                  " SU05        SU0$ )a  Didn't win's own saved views (2026-08-30), the smaller sibling of
_purchase_page_settings above: this page has exactly one setting of
its own, so no per-key branching is needed, matching
_integration_settings' own simplicity rather than the purchases
page's bigger multi-key shape.rF  rˆ  )r   rÛ  r   rK  )r6  rg  r•  s      r(   rN  Ú'CockpitHandler._lost_bids_page_settings-  sE   € ð  AÓ%ÜÐ;Ó<Ð<Ü)¨!Ð,=Ñ*>Ó?ˆÜ
�ŠÐ*¨EÐ2Ô3Ø! 5Ð)Ð)r*   c                 ól  • S H*  u  p4UR                  U5      S;  d  M  [        X   U5        M,     S H;  u  p4UR                  U5      S;  d  M  [        X   USS9S:  d  M/  [        U S35      e   U=(       d    [        [        SS	5      =(       d    S
n[
        R                  " XS9nU[        5       S.$ )a¥  A car bought outside the platforms (Steven 2026-08-27, "if we
bought a car from a local trader"). Validation lives in
db.add_manual_purchase; the numbers here just get the same
positive number treatment every other money field on this server
already has.

The fee fields allow zero (2026-08-31): a genuinely free
collection is a real answer, and rejecting "0" while accepting a
blank was a papercut with no reason behind it. Price, mileage and
the retail estimate stay strictly positive, where zero would be a
mistake rather than a fact. VAT is no longer accepted at all
(2026-08-31), it is computed from each fee's own VAT flag inside
db.add_manual_purchase rather than typed.))rR  zPurchase price)r-   ÚMileage)rR   r]  rÆ  ))rã  ÚFee)r&  Ú	TransportFr   r   z cannot be negative.r   r    r\  rŠ  )r!  rC  )r9   r  r   r$   r%   r   Úadd_manual_purchaserJ   )r6  rg  r#  r’   r¦   rø  s         r(   rü  Ú#CockpitHandler._add_manual_purchase9  s«   € ó?‰JˆCà�u‰u�S‹z Õ+Ü�Q‘V˜UÖ#ñ?ó G‰JˆCØ�u‰u�S‹z Õ+Ü˜™ °Ñ6¸Õ:Ü$¨ wÐ.BÐ%CÓDÐDñ Gð ×G”Wœ]¨M¸2Ó>×GÀ%ˆÜ×$Ò$ QÑ0ˆØÔ(<Ó(>Ñ?Ð?r*   c                 ó  ^	• UR                  S5      =(       d    SR                  5       nUR                  S5      =(       d    SR                  5       m	U[        R                  ;  a  [	        SU< 35      eT	(       d  [	        S5      eU(       a  [        R
                  " U5      O[        R
                  " 5       n[        UR                  U5      =(       d    / 5      n[        U	4S jU 5       S5      nUb  XFS   S.$ UR                  T	S	S
.5        [        X505      n0 UEUEnU(       a2  [        R                  " SU0U5        [        R
                  " U5      T	S.$ [        R                  " SU05        [        R
                  " 5       T	S.$ )aA  Add one option to a purchases dropdown column without a trip to
Settings (2026-08-31: "i want to be able to add a new supplier in
the add a car screen that then becomes a repeatable supplier",
2026-08-31 widened to Person collecting the same way). The appended
list goes through the SAME _validate_purchase_dropdowns the
Settings page saves through, so an option created here is held to
identical rules, no second, looser path into the same setting.

A name that already exists (any casing) is a quiet pick of the
existing one, not a refusal (2026-08-31, Steven's own instruction for
the redesigned inline add control: typing a name you already have
should just use it). "value" in the response is always the
option's own real, already-saved casing, since that is what the
caller has to select by, not necessarily what was typed.

path is only ever passed by a test wanting an isolated database,
same reasoning as _merged_settings above: db.py's own functions
default their path argument at definition time, so a caller cannot
rely on monkeypatching db.DB_PATH after the module has loaded.rï  r    r	  r…  za name is requiredc              3   ó¾   >#   • U  HR  n[        UR                  S S5      5      R                  5       R                  5       TR                  5       :X  d  MN  Uv •  MT     g7f)r	  r    N)r  r9   r¼   r  )rb   r˜  r	  s     €r(   re   Ú?CockpitHandler._add_purchase_dropdown_option.<locals>.<genexpr>o  sI   øé € ð V¢G˜qÜ §¡ g¨rÓ 2Ó3×9Ñ9Ó;×AÑAÓCÀuÇ{Á{Ã}ÑT÷ ™¢Gùs   ƒAAÁ	AN)Ú	dropdownsr	  rz  r‰  r\  )r9   r¼   r   r�  r   r(  r  rg  r  rc  rK  )
r6  rg  rð   r“  r  r”  Úexistingr•  r	  r	  s
            @r(   rû  Ú,CockpitHandler._add_purchase_dropdown_optionS  sO  ø€ ð( �u‰u�X‹×$ "×+Ñ+Ó-ˆØ—‘�w“×% 2×,Ñ,Ó.ˆØ”b×3Ñ3Ó3ÜÐ7¸±wÐ?Ó@Ð@ÞÜÐ1Ó2Ð2Þ59”"×+Ò+¨DÔ1¼r×?XÒ?XÓ?ZˆÜ�w—{‘{ 3Ó'×-¨2Ó.ˆÜô V¡Gó VØW[ó]ˆàÑØ!(¸GÑ3DÑEÐEØ�‰ °&Ñ9Ô:Ü,¨c¨^Ó<ˆØ%�GÐ%˜uÐ%ˆÞÜ�OŠOÐ1°6Ð:¸DÔAÜ!#×!:Ò!:¸4Ó!@È5ÑQÐQÜ
�ŠÐ-¨vÐ6Ô7Ü×6Ò6Ó8À5ÑIÐIr*   c                 óŠ   • [         R                  " UR                  SS5      5        [        5         SUR                  SS5      0$ )Nra   r    )r   Ú
unhide_carr9   r  rX  s     r(   r0  ÚCockpitHandler._unhide|  s5   € Ü
�Š�a—e‘e˜E 2Ó&Ô'ÜÔØ�q—u‘u˜U BÓ'Ð(Ð(r*   c                 ó:  • S Vs0 s H  o"UR                  U5      _M     nn[        R                  " UR                  SS5      UR                  SS5      [        UR                  SS5      5      UR                  SS5      UR                  SS5      UR                  S	S5      UR                  S
5      UR                  S5      US9	  [	        U5        U R                  5         [        5         UR                  SS5      [        UR                  SS5      5      S.$ s  snf )N)r.   r-   r1   ÚfuelÚ	body_typeÚ	cap_cleanr   ra   r    rq   r  Tr2   r3   r   Úmax_bidÚreserve)Údetails)ra   r  )r9   r   Úset_bidr�   r  Ú_kick_watchlist_syncr  )r6  rg  r¬   r§  s       r(   r1  ÚCockpitHandler._bid�  sð   € á]ó_Ú]ð $%�a—e‘e˜A“h’;Ù]ð 	ð _ä
�
Š
�1—5‘5˜ Ó# Q§U¡U¨;¸Ó%;¼TÀ!Ç%Á%ÈÈdÓBSÓ=TØ—5‘5˜ Ó$ a§e¡e¨G°RÓ&8¸!¿%¹%ÀÈÓ:KØ—5‘5˜Ó# Q§U¡U¨9Ó%5¸wò	Hô" 	˜QÔØ×!Ñ!Ô#ÜÔØ—u‘u˜U BÓ'¬t°A·E±E¸$ÀÓ4EÓ/FÑGÐGùò-_s   …Dc                 óÜ   • [        U5      n[        R                  " U5         SSKnSUl        [        5         UR                  5         [        5         S[        5       0$ ! [         a     Nf = f)aò  Save the buying rule overrides from the settings page. Raises
ValueError on anything that looks wrong (never guess a value, golden
rule 4 applies to what gets saved too), caught by do_POST and
returned as the usual {"ok": false, "error": ...} shape.

Also rebuilds cockpit.html from the last saved run right away (Mark
2026-08-25, added for theme colours specifically, so a colour saved
here shows on the cockpit immediately rather than only after the
next real run or a manual --render-only), the same cheap, local
only, no network rebuild the daily_run.py --render-only flag
already does. A settings save must still succeed even with no
cached run yet (a fresh install with nothing run once), so this is
best effort and never raises back to the caller.r   Nr  )
rv  r   rK  rg   rœ  ré   r�  r  rÞ   r  ©r6  rg  r•  rg   s       r(   rB  ÚCockpitHandler._settingsš  si   € ô # 1Ó%ˆÜ
�Š˜Ôð	Ûð )-ˆIÔ%ÜÔØ×!Ñ!Ô#Ü!Ô#ð Ô,Ó.Ð/Ð/øô ó 	Ùð	ús   £/A Á
A+Á*A+c                 óœ   • [        U5      n[        R                  " SU05         SSKnSUl        [        5         SU0$ ! [
         a     Nf = f)a  Save each automation's own enabled/disabled state from the
settings page's Integrations tab. A completely separate namespace
from the buying rule settings above, its own validation and its
own db.set_settings key, so a buying rule save can never touch
this and vice versa.r‹   r   N)r  r   rK  rg   rœ  rÞ   ré   r¬  s       r(   rC  Ú$CockpitHandler._integration_settings½  sV   € ô ' qÓ)ˆÜ
�Š˜¨Ð/Ô0ð
	ÛØ(,ˆIÔ%ô 	ÔØ Ð&Ð&øô ó 	Ùð	ús   ¥> ¾
AÁ
Ac                 ód  • [        UR                  S5      =(       d    SR                  5       [        UR                  S5      5      [        R
                  " 5       5      n[        R                  " U5         SSKnSUl        [        5         UR                  5         SUS   0$ ! [         a     Nf = f)a  Hide or show one connection from the settings page's Integrations
tab (Steven 2026-08-26). All the real logic lives in _apply_hide,
see its own docstring: hiding also switches the site's own
automations off, showing it again switches them back on. Rebuilds
cockpit.html right away, same best effort pattern as _settings, so
the Run modal's own baseline reflects the change without waiting
for the next real run (the modal also re-syncs itself live from
/api/settings every time it opens, so even a stale page is safe).r‘   r    r”   r   Nr‰   )rù  r9   r¼   r�   r   rU   rK  rg   rœ  ré   r�  rÞ   )r6  rg  rõ  rg   s       r(   rD  ÚCockpitHandler._hide_connectionÑ  s”   € ô ˜qŸu™u V›}×2°×9Ñ9Ó;¼TÀ!Ç%Á%ÈÃ/Ó=RÜ ŸošoÓ/ó1ˆä
�Š˜Ô ð	ÛØ(,ˆIÔ%ÜÔØ×!Ñ!Ô#ð % gÐ.BÑ&CÐDÐDøô ó 	Ùð	ús   Á6%B" Â"
B/Â.B/c                 ó  • UR                  S5      =(       d    SR                  5       nU(       d  [        S5      e[        R                  " X!R                  S5      =(       d    0 5      n[        5         [        R                  " 5       US.$ )zˆSave the current sidebar filter and sort state under a name (Mark
2026-08-20, the merged sort/view dropdown plus starrable saved views).r   r    z&Give the view a name before saving it.rÄ  )rM  r!  )r9   r¼   r   r   Ú	save_viewr  r$  )r6  rg  r   Úview_ids       r(   rE  ÚCockpitHandler._save_viewæ  sd   € ð —‘�f“×# ×*Ñ*Ó,ˆÞÜÐEÓFÐFÜ—,’,˜t§U¡U¨:Ó%6×%<¸"Ó=ˆÜÔÜ×,Ò,Ó.°gÑ>Ð>r*   c           	      óÔ   • [         R                  " [        UR                  S5      5      [	        UR                  SS5      5      5        [        5         S[         R                  " 5       0$ )Nr!  r`   TrM  )r   Ú	star_viewr‚   r9   r�   r  r$  rX  s     r(   rF  ÚCockpitHandler._star_viewð  sG   € Ü
�Š”S˜Ÿ™˜t›Ó%¤t¨A¯E©E°)¸TÓ,BÓ'CÔDÜÔØœ×,Ò,Ó.Ð/Ð/r*   c                 ó    • [         R                  " [        UR                  S5      5      5        [	        5         S[         R
                  " 5       0$ )Nr!  rM  )r   Údelete_viewr‚   r9   r  r$  rX  s     r(   rG  ÚCockpitHandler._delete_viewõ  s5   € Ü
�Š”s˜1Ÿ5™5 ›;Ó'Ô(ÜÔØœ×,Ò,Ó.Ð/Ð/r*   c                 ó~  ^• UR                  S5      nU[        R                  ;  d!  [        R                  U   R                  S:X  a  [	        SU< S35      e/ nUS:X  a‰  UR                  S5      nUbs  [        U[        5      (       a  U(       d  [	        S5      eU Vs/ s H  oU[        ;  d  M  UPM     nnU(       a  [	        SU S	[         35      eS
SR                  U5      /nGO;US:X  a�  UR                  S5      nUbk  [        U[        5      (       a  U(       d  [	        S5      eU Vs/ s H  oUS;  d  M
  UPM     nnU(       a  [	        SU S35      eU Vs/ s H  nSU 3PM
     nnGO´US:X  a  UR                  S5      (       a  S/nGO“US;   GaŒ  UR                  S5      =(       d    SR                  5       mT(       d  [	        S5      eT/nUS:X  GaH  UR                  S5      =(       d    / n[        U5      U R                  -
  nU(       a&  [	        SSR                  [        U5      5       35      e[        U4S j[        R                  " 5        5       S 5      n[        U5      [        [         R"                  5      -  n	[%        U5      =(       a*    [        U5      [        [         R&                  5      -
  (       + n
U	(       a  [        U5      U	-
  (       a  [	        S5      eU
(       aO  UbL  [        [        U5      5       H3  n[)        X‹[        R                  " 5       SS9nU(       d  M*  [	        U5      e   OÇUbÄ  UR                  S5      (       d  [	        T S 35      eUR                  S!5      (       a  [	        T S"35      eUR                  S#5      =(       d    0 nS$U;   a\  [         R*                  " U5      (       d-  UR                  S%5      b/  [         R,                  " U5      (       d  [	        [/        U5      5      eU(       a  US&SR                  U5      /-  n[1        X#U5      $ s  snf s  snf s  snf )'Nr’   r£   zUnknown run r¡   r•   r=  z@platforms must be a non empty list of platform names, or omittedzUnknown platform(s) z, expected some of r3  r¢  rC  r  z), expected some of ['motorway', 'carwow']r4  r5  r6  r7  )r‹  ró  Úpurchase_checkra   r    zreg is requiredr‹  r¾   zUnknown items: r­  c              3   ót   >#   • U  H-  oS    R                  5       TR                  5       :X  d  M)  Uv •  M/     g7f)ra   N)ra  )rb   r?   ra   s     €r(   re   Ú0CockpitHandler._start_run_api.<locals>.<genexpr>$  s/   øé € Ð^Ò': !À¹h¿n¹nÓ>NÐRU×R[ÑR[ÓR]Ñ>]ŸA™AÒ':ùs   ƒ(8¯	8zQA chip, check in or remove is sent on its own, not together with the other items.Tr‰  r-   r^  rZ  z& is a cancelled sale, nothing to push.rg  rT  rh  z--items)r9   r�   rŽ   Úscoper   r¹   r  r   r×  r¼   rl   Ú
_ROW_ITEMSr+  rg  r   rG   r   ÚFOLLOWUP_ONLYr�   ÚFOLLOWUP_ACTIONSrr  rm  ro  rn  r"  )r6  rg  r’   r?  r=  rè  Úbadr¾   rÌ   Úfollow_onlyÚas_followuprp  r”  Údc_rowra   s                 @r(   r’  ÚCockpitHandler._start_run_api   s°  ø€ Ø�e‰e�E‹lˆØ”e—j‘jÓ ¤E§J¡J¨s¡O×$9Ñ$9¸WÓ$DÜ˜|¨C©7°!Ð4Ó5Ð5ØˆØ�%‹<ØŸ™˜kÓ*ˆIØÑ$Ü! )¬T×2Ñ2¾)Ü$Ð%gÓhÐhÙ"+ÓF¢)˜Q¼Ñ/E—q¡)�ÐFÞÜ$Ð';¸C¸5Ð@SÔTaÐSbÐ%cÓdÐdØ&¨¯©°Ó(;Ð<�ùØ�KÓØŸ™˜kÓ*ˆIØÑ$Ü! )¬T×2Ñ2¾)Ü$Ð%gÓhÐhÙ"+ÓO¢)˜QÐ8NÑ/N—q¡)�ÐOÞÜ$Ð';¸C¸5Ð@iÐ%jÓkÐkÙ+4Ó5ª9 a˜2˜a˜S›©9�Ð5ùØ�KÓØ�u‰u�Z× Ñ Ø%˜�ùØÐSÔSØ—5‘5˜“<×% 2×,Ñ,Ó.ˆCÞÜ Ð!2Ó3Ð3Ø�EˆEØÐ*Ô*ØŸ™˜g›×,¨"�Ü˜%“j 4§?¡?Ñ2�ÞÜ$ °t·y±yÄÈÃÓ7MÐ6NÐ%OÓPÐPÜÔ^¤r×'8Ò'8Ô':Ó^Ð`dÓe�ô " %›j¬3¬×/LÑ/LÓ+MÑM�Ü" 5›k×f´3°u³:ÄÄO×DdÑDdÓ@eÑ3eÔ.f�Þ¤3 u£:°×#;Ü$ð &Jó Kð Kæ 3¡?ô #)¬¨U«Ö"4˜ô
 2°#¼r×?PÒ?PÓ?RÐZ^Ñ_˜ß˜3Ü",¨S£/Ð1ò #5ð ‘_ð Ÿ7™7 9×-Ñ-Ü(¨C¨5ð 1Zð *Zó [ð [à—w‘w˜~×.Ñ.Ü(¨C¨5Ð0VÐ)WÓXÐXð !ŸW™W ZÓ0×6°B�FØ 5Ó(¬o×.JÒ.JÈ6×.RÑ.RØ"ŸJ™JÐ':Ó;ÑGÔP_×PnÒPnÐou×PvÑPvÜ(Ô)>¸sÓ)CÓDÐDÞØ˜i¨¯©°%«Ð9Ñ9�EÜ˜# aÓ(Ð(ùòE Gùò Pùò 6s   ÂP0Â0P0Ä.	P5Ä;P5ÅP:c                 ó4   • U R                  0 UEUESU0E5      $ )Nr’   )r’  )r6  r’   rg  Úfixeds       r(   Ú_aliasÚCockpitHandler._aliasN  s%   € Ø×"Ñ"Ð#= aÐ#=¨5Ð#=°%¸Ñ#=Ó>Ð>r*   c                 ó&   • U R                  SU5      $ )Nr•   ©rË  rX  s     r(   r2  ÚCockpitHandler._runQ  s   € ˜dŸk™k¨%°Ó3Ð3r*   c                 ó&   • U R                  SU5      $ )Nr   rÎ  rX  s     r(   r3  ÚCockpitHandler._run_glassR  s   €  D§K¡K°¸Ó$;Ð;r*   c                 ó&   • U R                  SU5      $ )NÚclickdealerrÎ  rX  s     r(   r4  ÚCockpitHandler._run_clickdealerS  ó   € ¨$¯+©+°mÀQÓ*GÐ#Gr*   c                 ó&   • U R                  SU5      $ )NÚdealerkit_stockrÎ  rX  s     r(   r5  ÚCockpitHandler._run_dealerkitT  s   € ¨¯©Ð4EÀqÓ(IÐ!Ir*   c                 ó&   • U R                  SU5      $ )Nr§  rÎ  rX  s     r(   r6  Ú'CockpitHandler._run_dealerkit_purchasesU  s   € °$·+±+Ð>SÐUVÓ2WÐ+Wr*   c                 ó&   • U R                  SU5      $ )Nr‹  rÎ  rX  s     r(   r7  Ú"CockpitHandler._push_dealerkit_oneV  s   € ¨T¯[©[Ð9MÈqÓ-QÐ&Qr*   c                 ó&   • U R                  SU5      $ )Nró  rÎ  rX  s     r(   r8  Ú#CockpitHandler._check_dealerkit_oneW  s   € ¨d¯k©kÐ:OÐQRÓ.SÐ'Sr*   c                 ó&   • U R                  SU5      $ )NÚdealerkit_checkrÎ  rX  s     r(   r9  Ú#CockpitHandler._check_all_dealerkitX  s   € ¨d¯k©kÐ:KÈQÓ.OÐ'Or*   c                 ó&   • U R                  SU5      $ )Nr½  rÎ  rX  s     r(   r:  Ú"CockpitHandler._check_purchase_oneY  s   € ¨T¯[©[Ð9IÈ1Ó-MÐ&Mr*   c                 ó&   • U R                  SU5      $ )NrC  rÎ  rX  s     r(   r<  ÚCockpitHandler._run_purchasesZ  ó   € ¨¯©°KÀÓ(CÐ!Cr*   c                 ó&   • U R                  SU5      $ )Nr5  rÎ  rX  s     r(   r=  ÚCockpitHandler._run_lost_bids[  ræ  r*   c                 ó&   • U R                  SU5      $ )NÚ	compmatchrÎ  rX  s     r(   r>  ÚCockpitHandler._sync_compmatch\  ó   € ¨¯©°[À!Ó)DÐ"Dr*   c                 ó&   • U R                  SU5      $ )Nr’  rÎ  rX  s     r(   r?  ÚCockpitHandler._push_dealer_os]  s   € ¨¯©Ð5EÀqÓ)IÐ"Ir*   c                 ó&   • U R                  SU5      $ r-  rÎ  rX  s     r(   rJ  ÚCockpitHandler._sync_watchlist^  rì  r*   c                 ó&   • U R                  SU5      $ )Nr  rÎ  rX  s     r(   rK  ÚCockpitHandler._run_under_offer_  rÕ  r*   c                 ó&   • U R                  SU5      $ )NÚlecapital_fundingrÎ  rX  s     r(   r;  Ú%CockpitHandler._run_lecapital_funding`  s   € °·±Ð<OÐQRÓ0SÐ)Sr*   c                 ó<   •  [        S5        g! [         a     gf = f)a2  Best effort: start the background watchlist sync if nothing else
already has the shared browser profile, silently doing nothing
otherwise (2026-08-27). Called right after _bid() queues a star or
unstar. Never raises, a star toggle itself must never fail because
the platform side of it could not start yet.r.  N)r"  rÞ   r5  s    r(   r©  Ú#CockpitHandler._kick_watchlist_syncb  s!   € ð	Ü�{Õ#øÜó 	Ùð	ús   ‚ Ž
šc                 óÐ   • UR                  S5      nU[        ;  a   [        SU< S[        [        5       S35      e[	        SUS/5      nUR                  S5      (       a  UqX#S'   U$ )a»  Start a headed relogin window for one site, from the settings
page's Integrations tab (Mark 2026-08-24: "add a button under each
intergration to relogin"). Runs login.py <site> manual as its own
process, exactly what Mark would type by hand; manual mode means it
waits for the data/.login_done flag rather than trying to auto
detect a logged in state (only Motorway, Carwow and Auction4Cars
have that auto detection built into login.py, see its own
logged_in_url, none of the three sites relevant here do), so the
page's own "I'm logged in" button is what actually finishes it, via
_relogin_done. Blocked while any other headed pass is in flight,
same shared browser profile rule as everything else.r‘   rò  r  r¡   r£   Úmanualr�  )r9   Ú_RELOGIN_SITESr   r+  r"  rî   )r6  rg  r‘   r?   s       r(   r@  ÚCockpitHandler._reloginm  sl   € ð �u‰u�V‹}ˆØ”~Ó%Ü˜}¨T©HÐ4FÄvÌnÓG]ÐF^Ð^_Ð`ÓaÐaä�w  xÐ 0Ó1ˆØ�5‰5�×ÑØˆKØˆf‰IØˆr*   c                 ó@  • [        S5      (       d  [        S5      e[        R                  R	                  [
        SS5      n[        R                  " [        R                  R                  U5      SS9  [        USSS	9    S
S
S
5        SS0$ ! , (       d  f       SS0$ = f)aæ  Tell a running login.py process Mark has finished logging in, by
writing the same data/.login_done flag file it already waits on
(its own docstring: "a small flag file is created... when Steven
says so"; previously that was always a Claude session creating it
on his word in chat, this is the same signal from a button
instead). Raises if no relogin window is actually open, so a stray
click cannot silently do nothing useful, or worse, pre create the
flag for a window not started yet.r£   zNo relogin window is open.r   z.login_doneTrÍ  rÕ   rÖ   r×   Nr°   )	r¨   r   rï   rð   r×  rØ  rÙ  r»  rÚ   )r6  rg  r™  s      r(   rA  ÚCockpitHandler._relogin_doneƒ  s}   € ô ˜× Ñ ÜÐ9Ó:Ð:Ü�w‰w�|‰|œD &¨-Ó8ˆÜ
�Š”B—G‘G—O‘O DÓ)°DÒ9Ü�$˜ gÓ.Ø÷ /à˜ˆ~Ð÷ /Ô.à˜ˆ~Ðús   Á>BÂ
Bc                 ó8  • [        5       (       a  [        S5      e[        5       u  p#U(       d  [        U=(       d    S5      eU[        5       :X  a  SSUS.$ [        R
                  " / SQ[        SSSS9R                  R                  5       nU(       a  [        S	5      e[        R
                  " / S
Q[        SSSS9nUR                  S:w  a9  [        SUR                  =(       d    UR                  R                  5        35      e[        R
                  " SSU/[        SSSS9nUR                  S:w  a<  [        SU SUR                  =(       d    UR                  R                  5        35      e[        R                  " SS 5      R                  5         SUSS.$ )a|  One click update, from the settings page's Software update card
(Mark 2026-08-24: "a button to trigger move to latest version").
Fetches tags, checks out the newest real GitHub Release (never a
raw local tag that might exist without ever having been released,
same reasoning as _latest_version), then restarts this very server
process so the new code actually takes effect, the same
launchctl kickstart -k done by hand after every release change
this whole session. Blocked while any headed pass is in flight,
the same shared browser profile rule as everything else, a git
checkout mid run risks a half read module on disk. Refuses
outright, rather than guesses, if the working tree is not clean
(should never happen on a real install, dealer_config.py and every
saved session file are gitignored), a checkout over real local
changes is not something this button should ever silently
discard.zEA run or login is in progress. Wait for it to finish, then try again.z"Could not find the latest release.FT)rH  Úalready_latestrI  )rÕ  rš  z--porcelainz--untracked-files=norØ  rÙ  zpThe working tree has local changes, not updating automatically. Check git status on the Mac before trying again.)rÕ  rÊ  z--tagsry   r   zgit fetch failed: rÕ  Úcheckoutzgit checkout z	 failed: g      ø?c                  ó.   • [         R                  " S5      $ )Nr   )rï   Ú_exitrè   r*   r(   Ú<lambda>Ú(CockpitHandler._update.<locals>.<lambda>À  s   € ¤R§X¢X¨a¤[r*   )rH  rI  Ú
restarting)rí  r   râ  rÞ  r¿  r•   rØ  rµ  r¼   rá  r¶  Ú	threadingÚTimerÚstart)r6  rg  rŠ  ré  ÚdirtyrÊ  r   s          r(   rK  ÚCockpitHandler._update”  sr  € ô  ×ÑÜÐdÓeÐeÜ%Ó'‰ˆÞÜ˜S×HÐ$HÓIÐIØÔ%Ó'Ó'Ø$¸ÈÑPÐPô
 —’ÒWÔ]aØ.2¸ÀrñKßKQÉ6×RWÑRWÓRYð 	æÜð Pó Qð Qä—’Ò9¼tØ.2¸ÀrñKˆà×Ñ˜qÓ ÜÐ1°5·<±<×3OÀ5Ç<Á<×2VÑ2VÓ2XÐ1YÐZÓ[Ð[Ü—>’> 5¨*°fÐ"=Ä4Ø15¸DÈ"ñNˆà×Ñ !Ó#Ü˜}¨V¨H°I¸x¿¹×?aÐRZ×RaÑRa×>hÑ>hÓ>jÐ=kÐlÓmÐmô
 	�Š˜Ñ0Ó1×7Ñ7Ô9Ø¨FÀ$ÑGÐGr*   c           	      ó–  • SSK n/ n[           [        [        5       H?  n[	        U5      (       d  M  [        [        U   5      (       d  M.  UR                  U5        MA     SSS5         [        [        SSS9 n[        R                  " SSSS[        R                  " 5       S.U5        SSS5        U HC  n[        R                  U   R                  S	:X  d  M&   [         R"                  " US
[$        5        ME     ['        5         SU0$ ! , (       d  f       N®= f! , (       d  f       Nv= f! [         a     N…f = f! [         a     M”  f = f)a¯  Stop whatever is running. Safe for the data: every pass only
rewrites its cache and pages at the very end, so stopping partway
leaves the existing list untouched. Each was started in its own
session, so kill the whole process group to take its browser with
it. SIGTERM kills the child before its own handlers can run, so the
stop is recorded HERE for every bulk pass (a row job's stop stays
on its own page, a login has no record).r   NrÕ   rÖ   r×   r·   z-Run stopped. Your existing list is unchanged.)r™   r�   r°   rÙ   r›   ÚbulkF)rõ  r¹  r  ræ  r¨   rü  r  rÚ   rÛ   ri   rÜ   rÝ   rÞ   r�   rŽ   rÀ  r   Úrecord_run_statusrÄ   ré   )r6  rg  rõ  r·   r’   rß   s         r(   rC  ÚCockpitHandler._stop_runÃ  s  € ó 	ØˆÞÜœF–|�Ü —}‘}ÙÜ!¤&¨¡+×.Ñ.ÙØ—‘˜sÖ#ñ $÷ ð	Ü”h ¨gÒ6¸!Ü—	’	 IØ&UØ#$¨q¼¿	º	»ñEàFGôI÷ 7ó ˆCÜ�z‰z˜#‰×$Ñ$¨Õ.ðÜ×(Ò(¨¨e´]ÖCñ ô 	ÔØ˜7Ð#Ð#÷- �Yú÷ 7Õ6ûô ó 	Ùð	ûô !ó ÚðúsN   �ADÁ)D* Á81DÂ)D* ÃD:Ä
DÄ
D'Ä#D* Ä'D* Ä*
D7Ä6D7Ä:
EÅEc                 ó   • g rå  rè   )r6  Úargss     r(   Úlog_messageÚCockpitHandler.log_messageå  s   € Ør*   rè   rå  )NÚapp)?r9  r:  r;  r<  Ú__doc__r  r  r*  rT  r/  rH  rI  r�  rL  rM  rõ  rö  r÷  rú  rø  r  rN  rü  rû  r0  r1  rB  rC  rD  rE  rF  rG  rl   r   rù  rÃ  rÁ  r’  rË  r2  r3  r4  r5  r6  r7  r8  r9  r:  r<  r=  r>  r?  rJ  rK  r;  r©  r@  rA  rK  rC  r  r=  Ú__classcell__)r  s   @r(   r@  r@  �	  s   ø† ÙOõò"õt òl2Còh)ò5ò(5ò@ò5ò3ô"5ôHô.5ô&5ô 5ô/&òb
*ô@ô4'JòR)ò
Hò2!0òF'ò(Eò*?ò0ò
0ñ �_×/Ñ/Ó0±3°×7WÑ7WÓ3XÑX€JòL)ò\?ò 4Ú;ÚGÚIÚWÚQÚSÚOÚMÚCÚCÚDÚIÚDÚGÚSò	òò,ò"-Hò^ $÷Dð r*   r@  c                  óD  • [         R                  " 5         [        R                  " [        SSS9R                  5         [        R                  " [        SSS9R                  5         [        R                  " [        SSS9R                  5         [        R                  " [        [        S9n [        R                  R                  S[        4U 5       n[!        S[         S	35        [!        S
[         S35        UR#                  5         S S S 5        g ! , (       d  f       g = f)Nzdealer-os-syncT)Útargetr   Údaemonzdealerkit-stageszauto-update)Ú	directoryz	127.0.0.1z%BidBrain cockpit on http://localhost:z/cockpit.htmlz%Hidden cars page on http://localhost:z/hidden.html)r   Úinit_dbr  ÚThreadr¥  r  rª  r�  Ú	functoolsrš   r@  rØ  ÚhttpÚserverÚThreadingHTTPServerÚPORTr„  Úserve_forever)ÚhandlerÚhttpds     r(   Úmainr$  é  sÍ   € Ü‡J‚J„LÜ×ÒÔ0Ð7GÐPTÑU×[Ñ[Ô]Ü×ÒÔ1Ð8JÐSWÑX×^Ñ^Ô`Ü×ÒÔ-°MÈ$ÑO×UÑUÔWÜ×Ò¤¼$Ñ?€GÜ	�‰×	(Ñ	(¨+´tÐ)<¸gÔ	FÈ%ÜÐ5´d°V¸=ÐIÔJÜÐ5´d°V¸<ÐHÔIØ×ÑÔ÷ 
G×	FÖ	Fús   Ã7DÄ
DÚ__main__rå  )rÇ  )T)F)NF)zdocument.pdf)NNN)z	Dealer OSN)rè   N)¬r  r|   Úhttp.serverr  r  ri   rï   r±  rÁ  rÝ   r¿  r  rÈ  r;   ra  r   r   r   r   r   r   r�   Úbidbrain.pricingr	   r
   r   rg   r   r   r   r%   Úpurchases_runr   r+  r#  rð   r»  ÚabspathÚ__file__rØ  r   r×  rò   rÛ   r¼  ÚLockr¹  ræ  rî   rú  rë  r)   rD   rJ   rX   ru   ÚACTIVE_PHASESr¿   rÀ   rÂ   r  ÚPLATFORM_LABELSrŽ   Ú	label_forrÃ   rÄ   r‡   r—   rÏ   rÁ   rà   rä   rã   ræ   ré   r÷   rþ   r  r  r  r*  r,  r1  r-  Úcompiler4  r7  r:  r9  r;  rv  r‘  rc  rd  re  rÑ  rÛ  rf  r¨   rí  ró  rü  rõ   r  r	  r
  r=  rc  rd  rP  rQ  rh  re  r  Ú	frozensetrP  r'  r1  rA  rH  rN  rQ  rý  rc  rr  rn  r{  r|  r~  r†  r-  r—  r*  r­  rµ  rÞ  rÓ  r…  r  r  r  r  r[  rn  rr  rt  rv  rƒ  rV  r‡  r‹  r�  r¥  r©  rª  r"  rÍ  rÓ  rÞ  râ  rJ  r‚  r  r�  r`  rù  r  r  ÚSimpleHTTPRequestHandlerr@  r$  r9  )r?   r¬   s   00r(   Ú<module>r2     s¢  ðñó Û Û Û Û 	Û 	Û 
Û Û Û Û Û ç IÕ IÝ "ß IÑ Iß TÑ TÛ Ý -á±Ó@²¨A œi±Ñ@ÑWaÓCbÒWaÐRSÀfÄIÑWaÑCbÑbÓcÐ à	‡w�w‡��r—w‘w—‘ xÓ0Ó1€Ø€Ø�7‰7�<‰<˜˜f oÓ6€Ø�7‰7�<‰<˜˜fÐ&9Ó:€Ø�W‰W�\‰\˜$ ¨Ð0@ÓA€
ð �NŠNÓ€	Ø	€Ø€òl€ð ! K =Ñ0€ò-ò/ò2,ò ò> ð> ×$Ñ$€ð
 €Ø€ñ 37°u×7LÑ7LÓ2MÑPTÐUZ×U_ÑU_ÓP`Ò2`ÓaÒ2`¨Q�e—o‘o aÓ(Ò(Ñ2`Ña€ð )€ò,ò*1ô$s(òl	òð6 €
Ø 2 r (Ñ+€òò0òYô>ò+ô\	ò	ð !+°hÐP^Ø%5ÀKñQÐ ð +¨OÐ<Ð ò)òXð —
’
Ð/Ó0€ò4ð —’Øcóe€àÐ òò(VòrG€ò$òNò4ò:òz!òHò8.ò
,òò.òò ð\ Ð ØÐ ØÐ ð €Ø€Ø€ØÐ Ø—I’I“KÐ à€
ò&ñ $ò %ó Ð ð ,:ÈØ+9Ø0CØ'1ÀOØ'1ñ	3Ð ô";òJ
òKôò&ð  ¨8À+Ø!ð ¨KÀIÐYað $ñ
%€ò!ôH/òd
ið& €òòò
eòcòB
0ò1ò ôF	ô*?òZtò0d8ðVMÐ ð
5Ð ô iôFò"zjòz(!òVeð  RÑ(€ò"òðD Ð ò=ò(Qòò4cðP Ð òô8+2ò\:ò#ò0ò,ð2  TÑ*€òô0&òRUòò((òVô*e�T—[’[×9Ò9ô eòP	ð ˆzÓÙ…Fð ùòaf AùÒCbùòB bs   Á+M Á?M%
Æ	M*