Ë
    Š‰Œj, ã                  ó–  — d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlmZmZ ddlmZ ddlmZmZ ddlmZmZmZmZ ddlmZmZ ddlZdd	lmZ dd
lm Z   ejB                  d«      Z" e#«       Z$ ee%«      jM                  «       jN                  jN                  Z(dZ)dZ*dZ+dZ,dZ-dZ.dZ/dZ0dZ1dZ2dZ3dZ4dZ5dZ6e(dz  dz  dz  Z7 ejB                  d«      Z8dZ9dZ:dZ;d Z<d!Z=d"Z>d#Z?d$Z@d%ZAd&ZBd6d'„ZCd7d8d(„ZDd9d)„ZE	 	 d:	 	 	 d;d+„ZF eGh d,£«      ZHd*dd-œ	 	 	 	 	 d<d.„ZI G d/„ d0e«      ZJd7d=d1„ZK G d2„ d3«      ZL G d4„ d5e«      ZMy)>uŽ  
Live feed server  â€”  Â§6 seam #3: engine â†’ renderer  (SSE, stdlib-only)

The demo serves the feed as static JSON consumed by core/web/feed.js `FileFeed`. Production pushes the
same payload live; the renderer switches to it purely via config (core/web/config.js `feed:'live'` â†’
app.js lazy-loads live/web/live_feed.js `LiveFeed`). The renderer is otherwise unchanged â€” both sources
honour the one BroadcastFeed contract.

Transport = **Server-Sent Events** over the Python stdlib threading HTTP server (no extra deps):
broadcasting is one-way (server â†’ many renderers), which is exactly SSE's shape, and the browser's
`EventSource` gives auto-reconnect for free. This server also serves the static broadcast files (from
the v3/broadcast root) so the page and the `/events` stream share one origin â€” no CORS, one command.

Wire messages (SSE `event:` types; `data:` is JSON â€” mirror the file feed's shape, sent as deltas):
    init  : {meta, course, teams, params, media, video}        # static race header (also the backlog head)
    delta : {sec, standings, positions, events, ais, program}  # one tick, from StreamingScorer.push_frame
    stale : {stale, reason, sinceS}                            # ON AIR but publishing nothing (or recovered)
    result: {winner, winnerName, winnerSec, ...}              # once the race finishes

`program` (when the driver computes it â€” `program_state()`) is the explicit presentation phase
(countdown / racing / result), so a client never infers liveness from standings alone.

Late joiners (e.g. OBS reconnecting) get the full backlog â€” the latest `init` plus every `delta`/`result`
so far â€” before live deltas resume, so a fresh client renders the whole race-so-far.

Sub-phase: B-B (live feed transport). Drive it with live/serve_live.py (replays the demo race through
StreamingScorer); the bounded-LLM editor (B-C) edits each delta's newly-finalised colour beats first.
é    )ÚannotationsN)ÚSimpleHTTPRequestHandlerÚThreadingHTTPServer)ÚPath)Ú	HTTPErrorÚURLError)Úparse_qsÚquoteÚunquoteÚurlsplit)ÚRequestÚurlopen)Úcontrol_enabled)ÚRendererRegistryz^bytes=(\d*)-(\d*)$z/eventsz/client-statsz/commentary-playbacki   i    z/control/excursionz/control/course-guidez/control/camera-slotz/stats.json)z/healthzz/livezg      @g     €K@z/core/web/cesium-config.jsÚcoreÚwebzcesium-config.jsz/^/v3/view/([^/]+)/boats/([^/]+)/playlist\.m3u8$ÚVR_PRODUCER_READ_BASEÚVR_PRODUCER_READ_TOKENzX-VR-Producer-Tokeng       @g      $@z/standings.jsong     €f@ÚVR_STANDINGS_URLÚVR_STANDINGS_SOURCEÚVR_STANDINGS_TEAM_LABELSc                óZ   — d| › dt        j                  |d¬«      › d�j                  «       S )z#Encode one Server-Sent Event frame.zevent: z
data: F)Úensure_asciiz

)ÚjsonÚdumpsÚencode)ÚeventÚdatas     úC/srv/vr/releases/25f455cc4e75/v3/broadcast/live/live_feed_server.pyÚ_sser    ™   s,   € à�U�G˜8¤D§J¡J¨tÀ%Ô$HÐ#IÈÐN×UÑUÓWÐWó    c                óÐ  — g }| xs g D ]y  }|j                  d«      xs g D ]_  }|j                  d«      }|r|j                  d«      rŒ(|�||vrŒ/|j                  d«      }	 |j                  dt        |«      df|f«       Œa Œ{ |j                  «        |D ��cg c]  \  }}|‘Œ	 c}}S # t        t        f$ r& |j                  ddt        |xs d«      f|f«       Y Œ½w xY wc c}}w )uõ  Which boats stand in camera slots A, B, C â€¦ â€” the RACE PLANE's answer, published in the header.

    A camera slot is a POSITION an operator learns ("bottom-left is slot C"), so what fills it has to
    be stable for the whole heat: ORDERED BY SAIL NUMBER, ascending. Sorting by deviceId puts
    `boat:10` ahead of `boat:9`; sorting by anything that moves â€” place, distance to finish â€” would
    have boats swapping tiles under the operator's eyes while the race is sailed.

    Spectators never take a slot. They are on the water but not in this race, and a panel showing
    them is showing the wrong four boats.

    `camera_boats` restricts the order to hulls with a camera the picture can actually use; None
    means "every boat racing".
    ÚboatsÚdeviceIdÚ	spectatorÚsailIdr   Ú é   )ÚgetÚappendÚintÚ	TypeErrorÚ
ValueErrorÚstrÚsort)ÚteamsÚcamera_boatsÚrowsÚteamÚboatÚdidÚsailÚ_keys           r   Úcamera_slot_orderr8   ž   sñ   € ð 46€DØ’˜ò <ˆØ—X‘X˜gÓ&Ò,¨"ò 
	<ˆDØ—(‘(˜:Ó&ˆCÙ˜$Ÿ(™( ;Ô/ØØÐ'¨C°|Ñ,CØØ—8‘8˜HÓ%ˆDð<Ø—‘˜a¤ T£¨BÐ/°Ð5Õ6ñ
	<ð<ð 	‡I�I„KØ!%×&‘I�D˜#ŠCÓ&Ð&øô œzÐ*ò <Ø—‘˜a ¤C¨ª
°£OÐ4°cÐ:Ö;ð<üó 's   Á"B*ÂC"Â*2CÃCc                ó"  — t        | t        «      rd| v r| S |xs i j                  «       D ��ch c]
  \  }}|sŒ	|’Œ }}}t        | j	                  d«      xs d«      |z  }|rt        ||«      ng }|st        |d«      }|ri | ¥d|i¥S | S c c}}w )zÅ`meta` plus `cameraSlots`, so a tile can ask for a POSITION (`boatcam.html?slot=A`) instead of
    naming a hull that will not be racing an hour later. A caller that supplied its own is left alone.ÚcameraSlotsÚavailableOnboardCameraBoatIds© N)Ú
isinstanceÚdictÚitemsÚsetr)   r8   )Úmetar0   ÚmediaÚdevÚclipsÚ
with_clipsÚknownÚslotss           r   Úwith_camera_slotsrH   ½   s¢   € ô �dœDÔ! ]°dÑ%:ØˆØ).ª°"×(;Ñ(;Ó(=×G™*˜#˜uÂ’#ÐG€JÑGÜ�—‘Ð8Ó9Ò?¸RÓ@À:ÑM€EÙ/4Ô˜e UÔ+¸"€EÙô
 " %¨Ó.ˆñ .3Ð)ˆdÐ)�M 5Ñ)Ð<¸Ð<ùó Hs
   ®
B¹Bé´   c           	     ó<  — |rt|�r|D �cg c]:  }|j                  d«      t        v r!|j                  d«      �|d   |z
  dz  |d   f‘Œ< }}|r+|D �cg c]  }|d   |  k  sŒ|‘Œ }}|rt        |«      d   S dS 	 ddlm}  ||«      D �	�
cg c]*  \  }	}
|	t        |
d«      r|
j                  n
t        |
«      f‘Œ, }}	}
|D �	�
cg c]  \  }	}
|	|  k  sŒ|	|
f‘Œ }}	}
|rt        |«      d   S dS c c}w c c}w c c}
}	w # t        $ r | d	f|d
z
   dfddg}Y ŒYw xY wc c}
}	w )u,  Which start-sequence signal has most recently gone, at `to_gun_s` seconds before the gun.

    A commentator cannot say what moment the race is in without this, and until now nothing could:
    the warning, preparatory and one-minute signals are sounded by the hooter and beeped by the
    renderer, but nothing ever published WHICH of them had passed.

    Prefers the committee's own record (`raceSignals`) when the feed carries it â€” that is what
    actually happened, including a sequence that was started late. Falls back to the scheduled
    offsets, because a race running its sequence has passed those marks whether or not anyone logged
    them. The offsets come from `hooter.schedule`, which is what the horn itself uses: a second copy
    of RRS 26's timing here is a second thing to get wrong.
    NÚsignalÚtMsç     @�@r   r(   )Ústart_sequence_offsetsÚvalueÚwarningé<   Úpreparatory)iÄÿÿÿÚ
one_minute)r   Ústart)	r)   Ú_SEQUENCE_SIGNALSÚmaxÚv3.hooter.schedulerN   ÚhasattrrO   r.   Ú	Exception)Úto_gun_sÚpre_start_sÚsignalsÚ
gun_utc_msÚsÚrecordedÚentryÚgonerN   ÚoffÚsigÚoffsetsÚpasseds                r   Úlast_start_signalrf   Ñ   si  € ñ �:Ð)àMTö ZÈØ—u‘u˜X“Ô*;Ñ;ÀÇÁÀeÃÐ@Xð ˜‘h Ñ+¨vÑ5°q¸±{ÒCð Zˆð Záð (0ÖI˜e°5¸±8À¸yÓ3H’EÐIˆDÐIÙ#'”3�t“9˜Q‘<Ð1¨TÐ1ð6Ý=á#9¸+Ó#F÷HÙ�s˜Cð ¤g¨c°7Ô&;˜ŸšÄÀSÃÒJð Hˆñ Hð
 *1×E™X˜S #°C¸H¸9Ó4Dˆs�CŠjÐE€FÑEÙ#Œ3ˆv‹;�q‰>Ð-¨Ð-ùò!Zùò JùóHøäò 6Ø �L )Ð,°¸rÑ1AÐ/BÀMÐ.RØ&¨ð6Šð6üó FsA   ‰?C*ÁC/ÁC/Á8C: Â	/C4Â8C: Ã DÃDÃ4C: Ã:DÄD>   rT   rP   rS   rR   )r[   r\   c           
     óx   — | ||dz  z   z
  dz  }|rd}n
|dkD  rd}nd}|| |t        |d«      t        |||| «      dœS )	u«  The feed's EXPLICIT presentation phase for one tick â€” `{"state": "countdown"|"racing"|"result"}`.

    Clients must never have to infer "is this race live?" from standings alone: a stale finished
    leaderboard is visually identical to a live one (the "presentation stopped while the boats keep
    sailing" bug class, found 2026-07-09). Publishing the phase on every delta makes the distinction
    part of the wire contract: `result` = the shown race is decided (anything static on screen is
    intentional), `countdown` = pre-gun (the race clock counts down to the start), `racing` = live.

    The THIS-tick gun/track-start ride along so the client's race clock counts from the CURRENT race's
    gun. On a single race this equals the header gun; in a continuous pipeline (one fixed session gun so
    the client never reloads) each heat has its own gun, and the clock resets to it at each hand-over.

    `toGunS` and `signal` are what let the broadcast say WHERE IN THE RACE it is â€” "two minutes to
    the start", "the preparatory signal has gone". Every consumer used to re-derive the time to the
    gun from the two timestamps, and none of them could name the signal at all.
    éè  rM   Úresultr   Ú	countdownÚracingr(   )ÚstateÚgunUtcMsÚtrackStartUtcMsÚtoGunSrK   )Úroundrf   )r]   Útrack_start_utc_msÚsecÚfinishedr[   r\   rZ   rl   s           r   Úprogram_statert   ù   sb   € ð$ Ð0°3¸±:Ñ=Ñ>À&ÑH€HÙØ‰Ø	�AŠØ‰àˆØ¨
ÐGYÜ˜H aÓ(Ü'¨°+¸wÈ
ÓSñUð Ur!   c                  ó°   ‡ — e Zd ZdZˆ fd„Zd„ Zˆ fd„Zd„ Zˆ fd„Zd„ Z	ˆ fd„Z
dd	„Zdd
„Zdd„Zdd„Zdd„Zdd„Zdd„Zdd„Zdd„Zdd„Zdd„Zd„ Zˆ xZS )Ú_HandlerzIServes static broadcast files; intercepts GET /events for the SSE stream.c                óB   •— t        ‰| �  |dt        t        «      i|¤Ž y )NÚ	directory)ÚsuperÚ__init__r.   Ú_BROADCAST_ROOT)ÚselfÚargsÚkwargsÚ	__class__s      €r   rz   z_Handler.__init__  s   ø€ Ü‰Ñ˜$ÐI¬#¬oÓ*>ÐIÀ&ÓIr!   c                 ó   — y ©Nr<   )r|   r}   s     r   Úlog_messagez_Handler.log_message  s   € Ør!   c                ób   •— d| _         | j                  «       }|t        u rt        ‰| �  «       S |S )u†  Static files revalidate â€” a broadcast asset is EDITED IN PLACE under a fixed name.

        The bib atlas, a sail texture, the J/80 maps: the artwork changes, the URL never does, so a
        browser has nothing to tell one version from the next. `SimpleHTTPRequestHandler` sends
        `Last-Modified` and no `Cache-Control`, which is an invitation to HEURISTIC caching â€” a
        browser treats such a response as fresh for ~10% of its age, so an atlas last touched a
        fortnight ago is served from cache for a day and a half **without ever asking us**. Repaint a
        team's bib, reload, and the old one is still on the boat: it looks exactly like a broken
        renderer, and the OBS Browser source has no operator to press Ctrl+Shift+R.

        `no-cache` keeps the copy and forces a conditional request, which 304s until the bytes
        actually change â€” the cost of one round-trip per asset, in exchange for the picture always
        showing what is on disk.

        It also answers a byte RANGE, which `SimpleHTTPRequestHandler` does not: it ignores `Range`
        and replies 200 with the whole file. Most clients cope. Chromium's MEDIA stack does not â€” it
        range-requests every `<video>`, and given a 200 with no `Content-Range` it waits on a spinner
        for ever. That is exactly why the boat-cam clips played in an ordinary browser and rendered
        BLACK in OBS, whose CEF is stricter than Edge: same file, same server, same page, and the
        only difference was who asked for the bytes. Measured on a 900 kB clip â€” `Range: bytes=0-1023`
        came back `200` with all 895 kB and no `Content-Range`.
        T)Ú_static_responseÚ_send_range_headÚ	_NO_RANGEry   Ú	send_head)r|   Úrangedr   s     €r   r‡   z_Handler.send_head   s5   ø€ ð. !%ˆÔØ×&Ñ&Ó(ˆØ&,´	Ñ&9Œu‰wÑ Ó"ÐE¸vÐEr!   c           	     óð  — | j                   j                  d«      }|st        S t        j	                  |j                  «       «      }|st        S t        | j                  | j                  «      «      }|j                  «       st        S |j                  «       j                  }|j                  d«      |j                  d«      }}|dk(  r)|dk(  rt        S t        d|t        |«      z
  «      |dz
  }}nt        |«      }|rt        |«      n|dz
  }t        ||dz
  «      }||k\  s||kD  rP| j!                  d«       | j#                  dd|› �«       | j#                  d	d
«       d| _        | j'                  «        y|j)                  d«      5 }	|	j+                  |«       |	j-                  ||z
  dz   «      }
ddd«       | j!                  d«       | j#                  d| j/                  t1        |«      «      «       | j#                  dd«       | j#                  dd|› d|› d|› �«       | j#                  d	t1        t3        
«      «      «       | j'                  «        t5        j6                  |
«      S # 1 sw Y   ŒºxY w)uP  Answer `Range: bytes=â€¦` with a 206, or `_NO_RANGE` to fall through to the normal path.

        Clips are ~1 MB, so the slice is read into memory rather than streamed with a bounded
        reader â€” `copyfile` would otherwise send the whole file after our 206 headers, which is a
        worse bug than the one being fixed.
        ÚRanger(   é   r'   r   i   zContent-Rangezbytes */úContent-LengthÚ0FNÚrbéÎ   úContent-TypeúAccept-RangesÚbyteszbytes ú-ú/)Úheadersr)   r†   Ú	_RANGE_REÚmatchÚstripr   Útranslate_pathÚpathÚis_fileÚstatÚst_sizeÚgrouprV   r+   ÚminÚsend_responseÚsend_headerr„   Úend_headersÚopenÚseekÚreadÚ
guess_typer.   ÚlenÚioÚBytesIO)r|   ÚheaderÚmrš   ÚsizeÚfirstÚlastrT   ÚendÚfhÚchunks              r   r…   z_Handler._send_range_head;  s  € ð —‘×!Ñ! 'Ó*ˆÙÜÐÜ�O‰O˜FŸL™L›NÓ+ˆÙÜÐÜ�D×'Ñ'¨¯	©	Ó2Ó3ˆØ�|‰|Œ~ÜÐØ�y‰y‹{×"Ñ"ˆØ—g‘g˜a“j !§'¡'¨!£*ˆtˆØ�BŠ;Ø�rŠzÜ Ð Ü˜Q ¤s¨4£yÑ 0Ó1°4¸!±8�3‰Eä˜“JˆEÙ#”#�d”)¨°©ˆCÜ�#�t˜a‘xÓ ˆØ�DŠ=˜E CšKØ×Ñ˜sÔ#Ø×Ñ˜_°¸¸Ð.?Ô@Ø×ÑÐ-¨sÔ3Ø$)ˆDÔ!Ø×ÑÔØØ�Y‰Y�t‹_ð 	- Ø�G‰G�EŒNØ—G‘G˜C %™K¨!™OÓ,ˆE÷	-ð 	×Ñ˜3ÔØ×Ñ˜¨¯©¼¸T»Ó)CÔDØ×Ñ˜¨'Ô2Ø×Ñ˜¨F°5°'¸¸3¸%¸qÀÀÐ*GÔHØ×ÑÐ)¬3¬s°5«z«?Ô;Ø×ÑÔÜ�z‰z˜%Ó Ð ÷	-ð 	-ús   Æ
)I,É,I5c                ó’   •— t        | dd«      r+| j                  dd«       | j                  dd«       d| _        t        ‰| �  «        y )Nr„   FúCache-Controlúno-cacher‘   r’   )Úgetattrr¡   r„   ry   r¢   )r|   r   s    €r   r¢   z_Handler.end_headersg  sE   ø€ Ü�4Ð+¨UÔ3Ø×Ñ˜_¨jÔ9ð ×Ñ˜_¨gÔ6Ø$)ˆDÔ!Ü‰ÑÕr!   c                ó\  — | j                   j                  d«      d   j                  d«      }|t        t        t
        fv rÓt        | j                  j                  d«      xs d«      }i }d|cxk  r	t        k  r:n n7	 t        j                  | j                  j                  |«      xs d«      xs i }|t
        k(  r| j#                  |«      S |t        k(  r| j%                  d¬«      S | j'                  t)        |j                  dd«      «      ¬	«      S | j                   j                  d«      d   j                  d«      t*        k(  rÀt        | j                  j                  d«      xs d«      }d
}d|cxk  r	t        k  rOn nL	 | j,                  j/                  t        j                  | j                  j                  |«      xs d«      «      }| j1                  |rdnd«       | j3                  dd«       | j5                  «        y| j                   j                  d«      d   j                  d«      t6        k7  r4| j1                  d«       | j3                  dd«       | j5                  «        yt        | j                  j                  d«      xs d«      }d
}d|cxk  r	t        k  rYn nV	 | j,                  j8                  j;                  t        j                  | j                  j                  |«      xs d«      «      }| j1                  |rdnd«       | j3                  dd«       | j5                  «        y# t        j                  t        t         f$ r i }Y �Œ©w xY w# t        j                  t        t         f$ r d
}Y �Œ´w xY w# t        j                  t        t         f$ r d
}Y Œ¨w xY w)u¢  The renderers' back-channel. Nothing else posts here.

        Open in the same way `/events` is open â€” a renderer that can read the feed can report on
        itself, and under the World Sailing arrangement that renderer is on someone else's premises
        and holds no credential of ours beyond the feed token. The payload is untrusted and is
        clamped field by field in `RendererRegistry.record`.
        ú?r   r”   rŒ   s   {}T©ÚrequestÚfly©rº   Fs   nulléÌ   é�  r�   Né”  )rš   ÚsplitÚrstripÚ_CONTROL_EXCURSION_PATHÚ_CONTROL_COURSE_GUIDE_PATHÚ_CONTROL_CAMERA_SLOT_PATHr+   r•   r)   Ú_MAX_BEACON_BYTESr   ÚloadsÚrfiler¥   ÚJSONDecodeErrorÚUnicodeDecodeErrorr-   Ú_camera_slot_controlÚ_course_guide_controlÚ_excursion_controlÚboolÚ_COMMENTARY_PLAYBACK_PATHÚserverÚrecord_commentary_playbackr    r¡   r¢   Ú_CLIENT_STATS_PATHÚ	renderersÚrecord)r|   Úcontrol_pathÚlengthÚbodyÚoks        r   Údo_POSTz_Handler.do_POSTp  s  € ð —y‘y—‘ sÓ+¨AÑ.×5Ñ5°cÓ:ˆØÔ3Ô5OÜ5ð7ñ 7ä˜Ÿ™×)Ñ)Ð*:Ó;Ò@¸qÓAˆFØˆDØ�6Ô.Ô.Õ.ðÜŸ:™: d§j¡j§o¡o°fÓ&=Ò&FÀÓGÒMÈ2�Dð Ô8Ò8Ø×0Ñ0°Ó6Ð6ØÔ9Ò9Ø×1Ñ1¸$Ð1Ó?Ð?Ø×*Ñ*¬t°D·H±H¸UÀDÓ4IÓ/JÐ*ÓKÐKØ�9‰9�?‰?˜3Ó Ñ"×)Ñ)¨#Ó.Ô2KÒKÜ˜Ÿ™×)Ñ)Ð*:Ó;Ò@¸qÓAˆFØˆBØ�6Ô.Ô.Õ.ðØŸ™×?Ñ?ÜŸ
™
 4§:¡:§?¡?°6Ó#:Ò#E¸gÓFóH�Bð ×Ñ¡b™s¨cÔ2Ø×ÑÐ-¨sÔ3Ø×ÑÔØØ�9‰9�?‰?˜3Ó Ñ"×)Ñ)¨#Ó.Ô2DÒDØ×Ñ˜sÔ#Ø×ÑÐ-¨sÔ3Ø×ÑÔØÜ�T—\‘\×%Ñ%Ð&6Ó7Ò<¸1Ó=ˆØˆØˆvÔ*Ô*Õ*ðØ—[‘[×*Ñ*×1Ñ1´$·*±*¸T¿Z¹Z¿_¹_ÈVÓ=TÒ=_ÐX_Ó2`Óa�ð
 	×Ñ¡"™3¨#Ô.Ø×ÑÐ)¨3Ô/Ø×ÑÔØøôK ×,Ñ,Ô.@Ä*ÐMò Ø“Dðûô ×,Ñ,Ô.@Ä*ÐMò Ø“Bðûô  ×(Ñ(Ô*<¼jÐIò Ø’ðús8   Á>6L: ÆAM! Ê-AN Ì: MÍMÍ! NÎNÎ N+Î*N+c                óü  •— t        | j                  «      j                  }t        j                  |«      r| j	                  |«      S |j                  d«      t        v rgd| j                  j                  «       dœ}t        j                  j                  d«      xs dj                  «       }|r||d<   | j                  d|«      S |j                  d«      t        k(  r–| j                  «       s| j                  dd	d
i«      S | j                  d| j                  j                  «       | j                  j                   j#                  «       | j                  j                   j%                  «       dœ«      S |j                  d«      t&        k(  r| j)                  d ¬«      S |j                  d«      t*        k(  r| j-                  d¬«      S |j                  d«      t.        k(  r| j1                  d «      S |j                  d«      t2        k(  r| j5                  «       S |t6        k(  rH| j                  «       s| j                  dd	d
i«      S t8        j;                  «       s| j=                  «       S |j                  d«      t>        k(  ro| j                  «       sO| jA                  d«       | jC                  dd«       | jE                  «        | jF                  jI                  d«       y | jK                  «       S tL        ‰| ��  «       S )Nr”   T)rÖ   ÚclientsÚVR_BUILD_SHAr'   ÚbuildShaéÈ   é“  Úerrorúfeed token required)rÙ   rÑ   Ú	pageLoadsr»   Fr¸   r�   úapplication/jsons   {"error":"feed token required"})(r   rš   Ú_CAMERA_PLAYLIST_REr—   Ú_serve_camera_playlistrÀ   Ú_HEALTH_PATHSrÎ   Úclient_countÚosÚenvironr)   r˜   Ú_serve_jsonÚ_STATS_PATHÚ_feed_authorisedrÑ   ÚsnapshotÚloads_todayrÁ   rË   rÂ   rÊ   rÃ   rÉ   Ú_STANDINGS_PATHÚ_serve_standingsÚ_CESIUM_CONFIG_PATHÚ_CESIUM_CONFIG_FILEÚexistsÚ_serve_cesium_configÚ_EVENTS_PATHr    r¡   r¢   ÚwfileÚwriteÚ_serve_eventsry   Údo_GET)r|   rš   rÕ   Ú	build_shar   s       €r   r÷   z_Handler.do_GET§  sˆ  ø€ Ü˜Ÿ	™	Ó"×'Ñ'ˆÜ×$Ñ$ TÔ*Ø×.Ñ.¨tÓ4Ð4Ø�;‰;�sÓœ}Ñ,ð
 ¨4¯;©;×+CÑ+CÓ+EÑFˆDÜŸ™Ÿ™¨Ó7Ò=¸2×DÑDÓFˆIÙØ#,��ZÑ Ø×#Ñ# C¨Ó.Ð.Ø�;‰;�sÓœ{Ò*ð ×(Ñ(Ô*Ø×'Ñ'¨¨gÐ7LÐ-MÓNÐNð ×#Ñ# C°T·[±[×5MÑ5MÓ5OØ7;·{±{×7LÑ7L×7UÑ7UÓ7WØ7;·{±{×7LÑ7L×7XÑ7XÓ7Zñ*\ó ]ð ]ð �;‰;�sÓÔ6Ò6Ø×*Ñ*¨tÐ*Ó4Ð4Ø�;‰;�sÓÔ9Ò9Ø×-Ñ-°eÐ-Ó<Ð<Ø�;‰;�sÓÔ8Ò8Ø×,Ñ,¨TÓ2Ð2Ø�;‰;�sÓœÒ.Ø×(Ñ(Ó*Ð*ØÔ&Ò&ð ×(Ñ(Ô*Ø×'Ñ'¨¨gÐ7LÐ-MÓNÐNÜ&×-Ñ-Ô/Ø×0Ñ0Ó2Ð2Ø�;‰;�sÓœ|Ò+Ø×(Ñ(Ô*Ø×"Ñ" 3Ô'Ø× Ñ  Ð1CÔDØ× Ñ Ô"Ø—
‘
× Ñ Ð!CÔDØØ×%Ñ%Ó'Ð'Ü‰w‰~ÓÐr!   c           	     óÊ  — t         j                  |«      }|€| j                  dddi«      S d„ |j                  «       D «       \  }}| j                  }|j
                  5  ||j                  k(  xr ||j                  v }ddd«       s| j                  dddi«      S t        j                  j                  t        «      xs dj                  «       j                  d«      }t        j                  j                  t        «      xs dj                  «       }|r|s| j                  d	dd
i«      S ||f}	|j                  5  |j                   j#                  |	t%        j&                  «       «      }
ddd«       
5  t)        j*                  «       }|j                  5  |j,                  j                  |	«      }ddd«       �1||d   z
  t.        k  r"| j1                  |d   |d   d«      cddd«       S |�||d   z
  t2        k  r|nd}|› dt5        |d¬«      › dt5        |d¬«      › d�}t7        |t8        |i¬«      }	 t;        |d¬«      5 }|j=                  «       }|j>                  jA                  «       }ddd«       |j                  5  t)        j*                  «       f|j,                  |	<   ddd«       | j1                  d«      cddd«       S # 1 sw Y   �ŒQxY w# 1 sw Y   �ŒpxY w# 1 sw Y   �Œ6xY w# 1 sw Y   Œ…xY w# tB        $ rh}|�5|jD                  dv r'| j1                  |d   |d   d«      cY d}~cddd«       S | j                  |jD                  ddi«      cY d}~cddd«       S d}~wtF        tH        tJ        f$ rF |�$| j1                  |d   |d   d«      cY cddd«       S | j                  dddi«      cY cddd«       S w xY w# 1 sw Y   �Œ%xY w# 1 sw Y   yxY w)aÇ  Authenticate one current-race HLS playlist request on behalf of an OBS Browser Source.

        The allow-list comes from the init header already on air. It deliberately excludes arbitrary
        instances, devices, tracks and positions: knowing this service's URL is not a producer-read
        capability. The upstream base is deployment configuration rather than a request parameter,
        so this route cannot be turned into an open proxy.
        Nr¾   rÞ   zcamera playlist not foundc              3  ó2   K  — | ]  }t        |«      –— Œ y ­wr�   )r   ©Ú.0rO   s     r   ú	<genexpr>z2_Handler._serve_camera_playlist.<locals>.<genexpr>ê  s   è ø€ Ò!M°U¤'¨%§.Ñ!Mùs   ‚zcamera playlist not on airr'   r”   é÷  z!camera playlist proxy unavailabler   r(   r‹   z	fresh-hitz	/v3/view/)Úsafez/boats/z/playlist.m3u8?live=1)r•   g       @©Útimeout)i­  iô  éö  rþ   iø  zstale-if-errorz$camera playlist upstream unavailabler  z$camera playlist upstream unreachableÚupstream)&râ   r—   rè   ÚgroupsrÎ   Ú_lockÚ_playlist_instanceÚ_playlist_camerasræ   rç   r)   Ú_PRODUCER_READ_BASE_ENVr˜   rÀ   Ú_PRODUCER_READ_TOKEN_ENVÚ_playlist_cache_lockÚ_playlist_fetch_locksÚ
setdefaultÚ	threadingÚLockÚtimeÚ	monotonicÚ_playlist_cacheÚ_CAMERA_PLAYLIST_FRESH_SÚ_send_camera_playlistÚ!_CAMERA_PLAYLIST_STALE_IF_ERROR_Sr
   r   Ú_PRODUCER_READ_HEADERr   r¥   r•   Úget_content_typer   Úcoder   ÚOSErrorÚTimeoutError)r|   rš   r—   Úinstance_idÚ	camera_idÚsrvÚallowedÚbaseÚtokenÚ	cache_keyÚ
fetch_lockÚnowÚcachedÚstaler  r¹   ÚresponserÕ   Úcontent_typeÚexcs                       r   rã   z_Handler._serve_camera_playlistß  sì  € ô $×)Ñ)¨$Ó/ˆØˆ=Ø×#Ñ# C¨'Ð3NÐ)OÓPÐPÙ!M¸e¿l¹l»nÔ!MÑˆ�YØ"Ÿk™kˆØ�Y‰Yñ 	?Ø" c×&<Ñ&<Ñ<ò >Ø$¨×(=Ñ(=Ð=ð ÷	?ñ Ø×#Ñ# C¨'Ð3OÐ)PÓQÐQä—
‘
—‘Ô6Ó7Ò=¸2×DÑDÓF×MÑMÈcÓRˆÜ—‘—‘Ô 8Ó9Ò?¸R×FÑFÓHˆÙ™5Ø×#Ñ# C¨'Ð3VÐ)WÓXÐXØ  )Ð,ˆ	Ø×%Ñ%ñ 	[Ø×2Ñ2×=Ñ=¸iÌÏÉÓIYÓZˆJ÷	[ð
 ñ 	NÜ—.‘.Ó"ˆCØ×)Ñ)ñ <Ø×,Ñ,×0Ñ0°Ó;�÷<àÐ! c¨F°1©I¡oÔ9QÒ&QØ×1Ñ1°&¸±)¸VÀA¹YÈÓT÷	Nñ 	Nð  &Ð1Ø˜v a™y™Ô,MÒMñ ØSWð à˜& 	¬%°À"Ô*EÐ)FÀgÜ  °Ô4Ð5Ð5JðLˆHä˜hÔ1FÈÐ0NÔOˆGð`Ü˜W¨cÔ2ð G°hØ#Ÿ=™=›?�DØ#+×#3Ñ#3×#DÑ#DÓ#F�L÷Gð ×)Ñ)ñ XÜ26·.±.Ó2BÀDÈ,Ð1W�×#Ñ# IÑ.÷Xà×-Ñ-¨d°LÀ*ÓM÷;	Nñ 	N÷!	?ñ 	?ú÷	[ñ 	[ú÷<ñ <ú÷Gð Gûô ò eàÐ$¨¯©Ð5NÑ)NØ×5Ñ5°e¸A±hÀÀaÁÐJZÓ[Ô[÷'	Nñ 	Nð* ×'Ñ'¨¯©°7Ð<bÐ2cÓdÔd÷+	Nñ 	Nûô, œg¤|Ð4ò `ØÐ$Ø×5Ñ5°e¸A±hÀÀaÁÐJZÓ[Ñ[÷1	Nñ 	Nð2 ×'Ñ'¨¨gÐ7]Ð-^Ó_Ñ_÷3	Nñ 	Nð,`ú÷
Xñ Xú÷7	Nð 	NúsÍ   Á KÄ=/KÅ6!OÆK'Æ31OÇ.A	OÈ8L É+K4É0L É8OÊ%OÊ)OËKËK$Ë'K1	Ë,OË4K=	Ë9L Ì 	O	Ì	(M,Ì1O	Ì2OÍ M,ÍO	ÍOÍ,2O	ÎOÎ)O	Î=OÏO	Ï	OÏO	ÏOÏO"c                ó\  — | j                  d«       | j                  d|xs d«       | j                  dt        t        |«      «      «       | j                  dd«       | j                  d|«       | j                  dd	«       | j	                  «        | j
                  j                  |«       y
)zLSend one already-authorised playlist without duplicating response semantics.rÜ   r�   zapplication/vnd.apple.mpegurlrŒ   r³   úno-storezX-VR-Playlist-CacheúAccess-Control-Allow-OriginÚ*N)r    r¡   r.   r§   r¢   rô   rõ   )r|   rÕ   r&  Úcaches       r   r  z_Handler._send_camera_playlist  s’   € à×Ñ˜3ÔØ×Ñ˜¨Ò)XÐ9XÔYØ×ÑÐ)¬3¬s°4«y«>Ô:Ø×Ñ˜¨*Ô5Ø×ÑÐ.°Ô6Ø×ÑÐ6¸Ô<Ø×ÑÔØ�
‰
×Ñ˜ÔØr!   c                ó.  — | j                  «       s| j                  dddi«      S | j                  }|j                  5  t	        |j
                  «      }ddd«       |j                  j                  «      \  }}| j                  ||«      S # 1 sw Y   Œ9xY w)uÛ  `GET /standings.json` â€” the race office's published series table, cached.

        Behind the same gate as the feed (`_feed_authorised`, open when no `VR_FEED_TOKEN` is set),
        because the card that reads it is an OBS Browser source and a browser cannot mint anything
        better. The table itself is public information on the race office's own site; the gate is
        here so this route cannot become the one open door in a producer that has been shut.
        rÝ   rÞ   rß   N)rê   rè   rÎ   r  r>   Ú_presentation_factsÚ	standingsrë   )r|   r  ÚcontextÚstatusrÕ   s        r   rî   z_Handler._serve_standings'  s‰   € ð ×$Ñ$Ô&Ø×#Ñ# C¨'Ð3HÐ)IÓJÐJØ"Ÿk™kˆØ�Y‰Yñ 	4Ü˜3×2Ñ2Ó3ˆG÷	4à—}‘}×-Ñ-¨gÓ6‰ˆ�Ø×Ñ ¨Ó-Ð-÷	4ð 	4ús   ½BÂBc                óÌ  — t         j                  j                  d«      xs d}|s| j                  dd«      S dt	        j
                  |«      j                  «       z   dz   }| j                  d«       | j                  dd	«       | j                  d
t        t        |«      «      «       | j                  dd«       | j                  «        | j                  j                  |«       y)uw  `core/web/cesium-config.js`, generated from the environment when no file is on disk.

        The photoreal world needs a Cesium ion token, and that token used to reach production by
        being **on the disk of whoever ran the build**: the file is gitignored, so CI never had it and
        rendered stylised water, while a `gcloud builds submit` from a workstation baked it into the
        image and served it. Same tag, same command, a different picture, and nothing anywhere saying
        which â€” the image was no longer a function of the commit.

        So the build context excludes the file (`.gcloudignore`) and the DEPLOYMENT supplies the
        token, like every other secret. A file on disk still wins, because that is a developer's
        deliberate local choice; with neither, this 404s and `tiles.js` falls back to stylised water,
        exactly as it does today.ÚVR_CESIUM_ION_TOKENr'   r¾   z.no cesium-config.js and no VR_CESIUM_ION_TOKENsØ   // Generated from VR_CESIUM_ION_TOKEN by live_feed_server. Not a file on disk:
// the image must not carry this token, and the deployment must not depend on
// which machine built it.
export const CESIUM_ION_TOKEN = s   ;
rÜ   r�   ztext/javascriptrŒ   r³   r)  N)ræ   rç   r)   Ú
send_errorr   r   r   r    r¡   r.   r§   r¢   rô   rõ   )r|   r  rÕ   s      r   rò   z_Handler._serve_cesium_config7  sÍ   € ô —
‘
—‘Ð4Ó5Ò;¸ˆÙØ—?‘? 3Ð(XÓYÐYð4ô 7;·j±jÀÓ6G×6NÑ6NÓ6PñQð TZñZˆð 	×Ñ˜3ÔØ×Ñ˜Ð):Ô;Ø×ÑÐ)¬3¬s°4«y«>Ô:Ø×Ñ˜¨*Ô5Ø×ÑÔØ�
‰
×Ñ˜ÔØr!   c                óT  — t        j                  |«      j                  d«      }| j                  |«       | j	                  dd«       | j	                  dt        t        |«      «      «       | j	                  dd«       | j                  «        | j                  j                  |«       y )Nzutf-8r�   rá   rŒ   r³   r)  )
r   r   r   r    r¡   r.   r§   r¢   rô   rõ   )r|   r1  rÕ   Úraws       r   rè   z_Handler._serve_jsonS  s„   € Ü�j‰j˜Ó×%Ñ% gÓ.ˆØ×Ñ˜6Ô"Ø×Ñ˜Ð);Ô<Ø×ÑÐ)¬3¬s°3«x«=Ô9Ø×Ñ˜¨*Ô5Ø×ÑÔØ�
‰
×Ñ˜ÔØr!   c                ól  — t        «       syt        j                  j                  d«      xs dj	                  «       }|sy| j
                  j                  d«      xs d}|j                  «       j                  d«      r|dd j	                  «       nd}t        |«      xr t        j                  ||«      S )u(  TWO locks, and they refuse different mistakes.

        `BROADCAST_ALLOW_CONTROL` is the product's existing write gate â€” default OFF, an explicit act
        per deployment, set only on the box that is actually broadcasting. It is what stops a dev
        checkout from flying the camera on a live show.

        `$VR_BROADCAST_CONTROL_TOKEN` is a SECOND secret, and it is deliberately not `VR_FEED_TOKEN`:
        a partner rendering the picture for themselves holds the feed token, and being allowed to
        READ the feed must never amount to permission to STEER it. Unset â‡’ the route answers 503
        rather than opening, the same posture the MarkSetBot ingest takes â€” an unconfigured control
        is a closed door, not an open one, which is the opposite of the feed's own default.
        FÚVR_BROADCAST_CONTROL_TOKENr'   ÚAuthorizationúbearer é   N)r   ræ   rç   r)   r˜   r•   ÚlowerÚ
startswithrÌ   ÚhmacÚcompare_digest)r|   ÚwantÚauthÚgots       r   Ú_control_authorisedz_Handler._control_authorised]  s�   € ô Ô ØÜ—
‘
—‘Ð;Ó<ÒBÀ×IÑIÓKˆÙØØ�|‰|×Ñ Ó0Ò6°BˆØ"&§*¡*£,×"9Ñ"9¸)Ô"Dˆd�1�2ˆh�n‰nÔÈ"ˆÜ�C‹yÒ;œT×0Ñ0°°dÓ;Ð;r!   c                ó¼   — t        «       sddddœS t        j                  j                  d«      xs dj	                  «       sddddœS | j                  «       sdd	d
œS y)z¬Which of the two locks is shut, said apart. "Not configured" and "wrong key" need opposite
        actions, and a single 403 for both sends somebody to check the wrong one.rþ   z,broadcast control is not enabled on this boxÚBROADCAST_ALLOW_CONTROL)r  rÞ   Úhintr8  r'   zno control token is configuredi‘  zcontrol token required)r  rÞ   N)r   ræ   rç   r)   r˜   rC  ©r|   s    r   Ú_control_refusalz_Handler._control_refusals  sj   € ô Ô ØÐ*XØ5ñ7ð 7ä—
‘
—‘Ð;Ó<ÒBÀ×IÑIÔKØÐ*JØ8ñ:ð :à×'Ñ'Ô)ØÐ*BÑCÐCØr!   c               óL  — | j                  «       }|�!| j                  |j                  d«      |«      S t        | j                  dd«      }|€| j                  ddddœ«      S |€ | j                  d|j                  «       «      S | j                  d|j                  |¬	«      «      S )
z9Read or set the guided excursion. `fly=None` is the read.Nr  Ú	excursioné™  zno waiting picture is on airznot-waiting©rÞ   r  rÜ   r»   )rH  rè   Úpoprµ   rÎ   Úexcursion_stateÚrequest_excursion)r|   rº   ÚrefusalÚwaitings       r   rË   z_Handler._excursion_control€  s°   € à×'Ñ'Ó)ˆØÐØ×#Ñ# G§K¡K°Ó$7¸ÓAÐAÜ˜$Ÿ+™+ {°DÓ9ˆØˆ?ð ×#Ñ# CÐ3QØ2?ñ*Aó Bð Bàˆ;Ø×#Ñ# C¨×)@Ñ)@Ó)BÓCÐCØ×Ñ  W×%>Ñ%>À3Ð%>Ó%GÓHÐHr!   c               ó,  — | j                  «       }|�!| j                  |j                  d«      |«      S t        | j                  dd«      }|€| j                  ddddœ«      S |r|j                  «       n|j                  «       }| j                  d|«      S )	zFRead or request a course guide over the race that is currently on air.Nr  Úcourse_guiderK  zno race is currently on airzno-on-air-racerL  rÜ   )rH  rè   rM  rµ   rÎ   Úrequest_course_guideÚcourse_guide_state)r|   r¹   rP  Úracerl   s        r   rÊ   z_Handler._course_guide_control�  sš   € à×'Ñ'Ó)ˆØÐØ×#Ñ# G§K¡K°Ó$7¸ÓAÐAÜ�t—{‘{ N°DÓ9ˆØˆ<Ø×#Ñ# CÐ3PØ2Bñ*Dó Eð Eá/6�×)Ñ)Ô+¸D×<SÑ<SÓ<UˆØ×Ñ  UÓ+Ð+r!   c                ó   — | j                  «       }|�!| j                  |j                  d«      |«      S |€*| j                  d| j                  j	                  «       «      S t        |t        «      s| j                  dddi«      S 	 |j                  d«      r:| j                  d| j                  j                  |j                  d«      «      «      S | j                  j                  |j                  d	«      |j                  d«      «      }| j                  d|«      S # t        $ r(}| j                  d
t        |«      ddœ«      cY d}~S d}~ww xY w)z:Read or change the race producer's camera-slot assignment.Nr  rÜ   r½   rÞ   zJSON object requiredÚresolveOnlyr4   ÚslotrK  zinvalid-camera-assignmentrL  )rH  rè   rM  rÎ   Úcamera_slot_stater=   r>   r)   Úresolve_camera_boatÚassign_camera_slotr-   r.   )r|   rÕ   rP  ri   r'  s        r   rÉ   z_Handler._camera_slot_control›  s&  € à×'Ñ'Ó)ˆØÐØ×#Ñ# G§K¡K°Ó$7¸ÓAÐAØˆ<Ø×#Ñ# C¨¯©×)FÑ)FÓ)HÓIÐIÜ˜$¤Ô%Ø×#Ñ# C¨'Ð3IÐ)JÓKÐKð	cØ�x‰x˜Ô&Ø×'Ñ'¨¨T¯[©[×-LÑ-LÈTÏXÉXÐV\ÓM]Ó-^Ó_Ð_Ø—[‘[×3Ñ3°D·H±H¸VÓ4DÀdÇhÁhÈvÓFVÓWˆFð ×Ñ  VÓ,Ð,øô ò 	cØ×#Ñ# C´3°s³8ÐE`Ñ)aÓbÕbûð	cús%   ÂA
D Ã:D Ä	EÄ%EÅEÅEc                óà  — t         j                  j                  d«      xs d}|syd}| j                  j                  d«      xs d}|j	                  «       j                  d«      r|dd j                  «       }|sQd| j                  v r| j                  j                  dd	«      d	   nd}t        |«      j                  d
«      xs dgd   }t        |«      xr t        j                  ||«      S )u  Is this caller allowed to read the live feed?

        **Unset `VR_FEED_TOKEN` â‡’ open**, which is what a local run, the simulator and the recorded
        demo all rely on â€” and what this service did unconditionally until now. Set it, and the feed
        becomes a shared-secret surface.

        Why a token in the URL rather than Cloud Run IAM: the consumer is an **OBS Browser source**
        on the on-site render machine. A browser cannot mint a Google-signed ID token, so
        `--no-allow-unauthenticated` would not secure this feed â€” it would switch the broadcast off.
        A query parameter is the one channel an OBS source URL actually has. It is weaker than IAM
        (URLs get logged) and does not pretend otherwise; it is the difference between "anyone who
        guesses the hostname gets a real-time tactical feed of the championship" and "you need the
        secret", which is the gap worth closing before a title event.

        The header form is accepted too, for `curl` and any non-browser consumer.ÚVR_FEED_TOKENr'   Tr9  r:  r;  Nr·   r(   r  r   )ræ   rç   r)   r•   r<  r=  r˜   rš   r¿   r	   rÌ   r>  r?  )r|   r@  rB  rA  Úqss        r   rê   z_Handler._feed_authorised¬  sÊ   € ô  �z‰z�~‰~˜oÓ.Ò4°"ˆÙØØˆØ�|‰|×Ñ Ó0Ò6°BˆØ�:‰:‹<×"Ñ" 9Ô-Ø�q�r�(—.‘.Ó"ˆCÙØ/2°d·i±iÑ/?�—‘—‘  aÓ(¨Ò+ÀRˆBÜ˜B“<×#Ñ# GÓ,Ò4°°°aÑ8ˆCÜ�C‹yÒ;œT×0Ñ0°°dÓ;Ð;r!   c                ó   — | j                   }|j                  «       }t        j                  «       t        z   }	 | j                  d«       | j                  dd«       | j                  dd«       | j                  dd«       | j                  dd	«       | j                  «        |j                  «       D ]  }| j                  j                  |«       Œ | j                  j                  «        	 |t        j                  «       z
  }|dk  rd
| _        nZ	 |j                  t        t        |«      ¬«      }|€n6| j                  j                  |«       | j                  j                  «        Œ~|j'                  |«       y # t         j"                  $ rX t        j                  «       |k\  r	d
| _        Y ŒE| j                  j                  d«       | j                  j                  «        Y Œûw xY w# t$        $ r Y Œ‹w xY w# |j'                  |«       w xY w)NrÜ   r�   ztext/event-streamr³   r´   Ú
Connectionz
keep-aliver*  r+  Tr   r   s   : keepalive

)rÎ   Ú
_subscriber  r  Ú_SSE_SESSION_MAX_Sr    r¡   r¢   Ú_backlogrô   rõ   ÚflushÚclose_connectionr)   rŸ   Ú_KEEPALIVE_SÚqueueÚEmptyÚConnectionErrorÚ_unsubscribe)r|   r  ÚqÚdeadliner±   Ú	remainings         r   rö   z_Handler._serve_eventsÈ  s¿  € Ø"Ÿk™kˆØ�N‰NÓˆÜ—>‘>Ó#Ô&8Ñ8ˆð$	 Ø×Ñ˜sÔ#Ø×Ñ˜^Ð-@ÔAØ×Ñ˜_¨jÔ9Ø×Ñ˜\¨<Ô8Ø×ÑÐ:¸CÔ@Ø×ÑÔØŸ™›ò (�Ø—
‘
× Ñ  Õ'ð(à�J‰J×ÑÔØØ$¤t§~¡~Ó'7Ñ7�	Ø ’>Ø,0�DÔ)ØðØŸE™E¬#¬l¸IÓ*F˜EÓG�Eð �=ØØ—
‘
× Ñ  Ô'Ø—
‘
× Ñ Ô"ð- ð4 ×Ñ˜QÕøô' —{‘{ò Ü—~‘~Ó'¨8Ò3Ø04˜Ô-Ùð —J‘J×$Ñ$Ð%7Ô8Ø—J‘J×$Ñ$Ô&Ùðûô  ò 	Ùð	ûð ×Ñ˜QÕúsT   ¹CG+ Ä E= Ä29G+ Å=1G(Æ.G+ Æ05G(Ç%G+ Ç'G(Ç(G+ Ç+	G7Ç4G: Ç6G7Ç7G: Ç:H)rš   r.   ÚreturnÚNone)rÕ   r’   r&  ú
str | Noner,  r.   ro  rp  ©ro  rp  )r1  r+   rÕ   r>   ro  rp  )ro  rÌ   )ro  údict | None)rº   zbool | Nonero  rp  )r¹   rÌ   ro  rp  )rÕ   rs  ro  rp  )Ú__name__Ú
__module__Ú__qualname__Ú__doc__rz   r‚   r‡   r…   r¢   r×   r÷   rã   r  rî   rò   rè   rC  rH  rË   rÊ   rÉ   rê   rö   Ú__classcell__©r   s   @r   rv   rv     sm   ø„ ÙSôJòôFò6*!ôXò5ôn6 óp:Nóx
ó.ó ó8ó<ó,óIó
,ó-ó"<ö8( r!   rv   c                ó*  — | €t         j                  n| } | j                  t        «      xs dj	                  «       }|si S || j                  t
        «      xs dj	                  «       dœ}| j                  t        «      xs dj	                  «       }|r_	 t        j                  |«      }t        |t        «      r9|j                  «       D ��ci c]  \  }}t        |«      t        |«      “Œ c}}|d<   |S # t        j                  $ r d}Y Œbw xY wc c}}w )a  The `standings` binding, for a producer started without an event published.

    Shaped exactly like `event_config.integration_for(cfg, "standings")["config"]`, so the local
    override and the event's own declaration are the same object and only one of them can be wrong.
    Nr'   )Ú
rankingUrlÚdisplayNameÚ
teamLabels)ræ   rç   r)   Ú_STANDINGS_URL_ENVr˜   Ú_STANDINGS_SOURCE_ENVÚ_STANDINGS_LABELS_ENVr   rÅ   rÇ   r=   r>   r?   r.   )ÚenvÚurlÚbindingr6  ÚlabelsÚkÚvs          r   Ústandings_binding_from_envr‡  ó  sê   € ð ˜Œ"�*Š*¨€CØ�7‰7Ô%Ó&Ò,¨"×
3Ñ
3Ó
5€CÙØˆ	Ø °#·'±'Ô:OÓ2PÒ2VÐTV×1]Ñ1]Ó1_Ñ`€GØ�7‰7Ô(Ó)Ò/¨R×
6Ñ
6Ó
8€CÙ
ð	Ü—Z‘Z “_ˆFô �fœdÔ#Ø@FÇÁÃ×$O¹¸¸1¤S¨£V¬S°«V¡^Ó$OˆG�LÑ!Ø€Nøô	 ×#Ñ#ò 	ØŠFð	üó %Ps   ÂC4 ÃDÃ4DÄDc                  óP   — e Zd ZdZd	edddœ	 d
d„Zedd„«       Zd„ Zdd„Z	d	dd„Z
y)ÚPublishedStandingsu  The race office's published series table, cached in front of their site.

    Two rules, and both are about what the card shows when something is wrong.

    **A table never goes blank.** Once one has been read successfully it is served for as long as the
    producer lives, with its AGE stated â€” an hour-old table is still the standings, where an empty
    one says the championship has not started. Only a producer that has never managed a single read
    answers an error, because then there is genuinely nothing to show.

    **A fetch that is in flight is not started twice.** Several renderers poll this, and the OBS box
    plus a partner's box plus an operator's browser all landing on an expired TTL together must be
    one request to somebody else's server, not three.
    N)Úttl_sr¥   r"  c               ó  — t        |xs i «      | _        || _        || _        |xs t        j                  | _        t        j                  «       | _        t        j                  «       | _	        d | _
        d | _        d | _        y r�   )r>   Ú_bindingÚ_ttlÚ_readr  Ú_nowr  r  Ú_fetch_lockÚ_state_lockÚ_tableÚ_fetched_atÚ_error)r|   rƒ  rŠ  r¥   r"  s        r   rz   zPublishedStandings.__init__  sd   € ä˜Wš]¨Ó+ˆŒØˆŒ	ØˆŒ
ØÒ$œ4Ÿ9™9ˆŒ	Ü$Ÿ>™>Ó+ˆÔÜ$Ÿ>™>Ó+ˆÔØ#'ˆŒØ)-ˆÔØ"&ˆ�r!   c                ón   — t        | j                  j                  d«      xs d«      j                  «       S )Nr{  r'   )r.   rŒ  r)   r˜   rG  s    r   r‚  zPublishedStandings.url$  s*   € ä�4—=‘=×$Ñ$ \Ó2Ò8°bÓ9×?Ñ?ÓAÐAr!   c                ó$  — | j                   �| j                   S t        t        «      j                  «       j                  d   dz  }t        |«      t        j                  vr)t        j                  j                  dt        |«      «       ddl	m
} |S )u-  `ksail_public.read_ranking`, imported late.

        The producer must not take an import-time dependency on the ingest tree for a surface most
        runs never open â€” `serve_live.py`'s demo replay and every unit test would then need it on
        `sys.path` to import this module at all.
        r‹   Úingestr   )Úread_ranking)rŽ  r   Ú__file__ÚresolveÚparentsr.   Úsysrš   ÚinsertÚksail_publicr˜  )r|   r—  r˜  s      r   Ú_readerzPublishedStandings._reader(  si   € ð �:‰:Ð!Ø—:‘:ÐÜ”h“×'Ñ'Ó)×1Ñ1°!Ñ4°xÑ?ˆÜˆv‹;œcŸh™hÑ&Ü�H‰H�O‰O˜Aœs 6›{Ô+Ý-ØÐr!   c                óè  — 	  | j                  «       | j                  | j                  j                  d«      | j                  j                  d«      ¬«      }| j
                  5  || j                  «       d c| _
        | _        | _        d d d «       y # t        $ rK}| j
                  5  t        |«      j                  › d|› �| _        d d d «       n# 1 sw Y   nxY wY d }~y d }~ww xY w# 1 sw Y   y xY w)Nr}  ÚteamFullNames)Úteam_labelsÚteam_full_namesz: )rŸ  r‚  rŒ  r)   rY   r‘  Útypert  r”  r�  r’  r“  )r|   Útabler'  s      r   Ú_refreshzPublishedStandings._refresh7  sÛ   € ð	Ø"�D—L‘L“N 4§8¡8Ø/3¯}©}×/@Ñ/@ÀÓ/NØ37·=±=×3DÑ3DÀ_Ó3UôWˆEð ×Ññ 	RØ9>ÀÇ	Á	ÃÈTÐ6ˆDŒK˜Ô)¨4¬;÷	Rð 	Røô	 ò 	Ø×!Ñ!ñ =Ü!% c£×!3Ñ!3Ð 4°B°s°eÐ<�”÷=÷ =ñ =úäûð	ú÷	Rð 	Rús<   ‚AB Á#%C(Â	C%ÂC Â& CÃ	C ÃC	ÃC Ã C%Ã(C1c           
     óð  — | j                   sdddt        › �dœfS | j                  5  | j                  €dn| j	                  «       | j                  z
  }ddd«       �|| j
                  kD  rv| j                  5  | j                  5  | j                  €dn| j	                  «       | j                  z
  }ddd«       |�|| j
                  kD  r| j                  «        ddd«       | j                  5  | j                  | j                  | j                  }}}ddd«       €dddœfS i |¥t        | j                  j                  d	«      xs d
«      xs dt        dz  «      t        t        d| j	                  «       |z
  «      d«      dœ¥|xs i j!                  «       D ��ci c]  \  }}|sŒ	||“Œ c}}¥}r||d<   d|fS # 1 sw Y   �ŒnxY w# 1 sw Y   �ŒxY w# 1 sw Y   �ŒxY w# 1 sw Y   ŒÔxY wc c}}w )u¯   `(status, body)` for one request. `context` is what the producer knows and the race office
        does not â€” the event's name, and the venue zone the card stamps itself in.r¾   z-no standings source is bound to this producerz5publish the event's `standings` integration, or set $)rÞ   rF  Nr  z)the published standings could not be read)rÞ   Údetailr|  r'   rh   ç        r(   )Ú
sourceNameÚfetchedUtcMsÚageSÚstaleReasonrÜ   )r‚  r~  r‘  r“  r�  r�  r�  r¦  r’  r”  r.   rŒ  r)   r+   rp   rV   r?   )	r|   r0  Úager¥  Ú
fetched_atrÞ   r…  r†  rÕ   s	            r   rë   zPublishedStandings.snapshotC  sÿ  € ð �xŠxð Ð"QØ#XÔYkÐXlÐ!mñoð oð oà×Ññ 	WØ×*Ñ*Ð2‘$¸¿	¹	»Àd×FVÑFVÑ8VˆC÷	Wàˆ;˜# §	¡	š/Ø×!Ñ!ñ $Ø×%Ñ%ñ _Ø"&×"2Ñ"2Ð":™$ÀÇ	Á	ÃÈd×N^ÑN^Ñ@^�C÷_à�; #¨¯	©	¢/Ø—M‘M”O÷	$ð
 ×Ññ 	RØ'+§{¡{°D×4DÑ4DÀdÇkÁk˜u�:ˆE÷	Ràˆ=ØÐ"MÐY^Ñ_Ð_Ð_ð
Øð
ä˜dŸm™m×/Ñ/°Ó>ÒDÀ"ÓEÒMÈÜ 
¨TÑ 1Ó2ô œ#˜c 4§9¡9£;°Ñ#;Ó<¸aÓ@ò
ð ")¢¨B× 5Ñ 5Ó 7×=™˜˜1º1ˆq�!‰tÓ=ð
ˆñ Ø"'ˆD�ÑØ�DˆyÐ÷/	Wñ 	Wú÷_ñ _ú÷$ñ $ú÷
	Rð 	Rüó >sG   §,F?Á8GÂ,GÂ1)GÃ.%G&Æ
G2Æ*G2Æ?G	ÇG	ÇGÇG#Ç&G/r�   )rƒ  rs  rŠ  Úfloatro  rp  )ro  r.   rr  )r0  rs  ro  ztuple[int, dict])rt  ru  rv  rw  Ú_STANDINGS_FRESH_Srz   Úpropertyr‚  rŸ  r¦  rë   r<   r!   r   r‰  r‰  	  sG   „ ñð
'ÐFXØ ñ
'Ø)-ó
'ð òBó ðBòó
Rõr!   r‰  c                  ó<  ‡ — e Zd ZdZdZdZdZdZd"ddedddœ	 	 	 d#ˆ fd„Z	e
d$d„«       Zd%d	„Zd&d
„Zd'd„Zed(d„«       Zd(d„Zˆ fd„Zd)d„Zd*d„Zd+d„Zddœd,d„Zd-d.d„Z	 	 d/	 	 	 d0d„Z	 	 	 d1	 	 	 	 	 	 	 	 	 d2d„Zd3d„Zd4d„Zdddœd5d„Zd6d„Zd7d„Zd8d „Zd!„ Z ˆ xZ!S )9ÚLiveFeedServerzÄBroadcasts init/delta/result SSE messages to connected renderer clients, and serves the static
    page. Thread-safe: `publish_*` may be called from the scoring/replay thread while clients stream.Ti   é   N)Úlive_backlogÚ
asrun_pathÚon_logÚnow_msr/  c               ó.  •— t         ‰| �  ||ft        «       t        «       | _        t        j                  «       | _        g | _        |�|n| j                  | _
        t        «       | _        d | _        g | _        g | _        d| _        d | _        t        «       | _        t        j                  «       | _        i | _        i | _        || _        || _        |xs d„ | _        i | _        i | _        i | _        t9        |xs
 t;        «       «      | _        i | _        y )Nr<   c                 óB   — t        t        j                  «       dz  «      S )Nrh   )r+   r  r<   r!   r   ú<lambda>z)LiveFeedServer.__init__.<locals>.<lambda>›  s   € ¬#¬d¯i©i«k¸DÑ.@Ó*A€ r!   ) ry   rz   rv   r@   Ú_clientsr  r  r  Ú_historyÚMAX_BACKLOGÚ_max_backlogr   rÑ   rS  Ú_camera_teamsÚ_camera_slotsÚ_camera_race_identityr  r  r
  r  r  Ú_asrun_pathÚ_on_logÚ_now_msÚ_commentary_identityÚ_commentary_variantsÚ_commentary_receiptsr‰  r‡  r/  r.  )	r|   ÚhostÚportr¶  r·  r¸  r¹  r/  r   s	           €r   rz   zLiveFeedServer.__init__}  s  ø€ ô 	‰Ñ˜$ ˜¤xÔ0Ü*-«%ˆŒÜ—^‘^Ó%ˆŒ
Ø%'ˆŒØ,8Ð,D™LÈ$×JZÑJZˆÔô *Ó+ˆŒð !ˆÔØ#%ˆÔØ(*ˆÔØ,.ˆÔ"Ø.2ˆÔÜ+.«5ˆÔÜ$-§N¡NÓ$4ˆÔ!ØLNˆÔ"ØWYˆÔð
 &ˆÔØˆŒØÒBÑ"AˆŒØ*,ˆÔ!ð 68ˆÔ!ØACˆÔ!ô ,¨IÒ,UÔ9SÓ9UÓVˆŒð *,ˆÕ r!   c                ó,   ‡ — t        ˆ fd„dD «       «      S )Nc              3  ó@   •K  — | ]  }‰j                  |«      –— Œ y ­wr�   ©r)   )rü   ÚkeyrA   s     €r   rý   z0LiveFeedServer._race_identity.<locals>.<genexpr>­  s   øè ø€ Ò\ s�T—X‘X˜c—]Ñ\ùs   ƒ)Ú
instanceIdrm   rn   )Útuple)rA   s   `r   Ú_race_identityzLiveFeedServer._race_identity«  s   ø€ äÓ\Ð.[Ô\Ó\Ð\r!   c           	     óÖ   — | j                   5  t        | j                  «      }d d d «       t        |«      D ��ci c]  \  }}t	        d|z   «      |“Œ c}}dœS # 1 sw Y   Œ7xY wc c}}w )NéA   )r:   Úassignments)r  ÚlistrÂ  Ú	enumerateÚchr)r|   rG   Úir4   s       r   rZ  z LiveFeedServer.camera_slot_state¯  sf   € Ø�Z‰Zñ 	-Ü˜×+Ñ+Ó,ˆE÷	-à$ÜAJÈ5ÓAQ×R±g°a¸¤ B¨¡F£¨TÑ 1ÓRñTð 	T÷	-ð 	-üó  Ss   �AºA%ÁA"c                ó^  — t        |xs d«      j                  «       j                  «       }t        |«      dk7  sd|cxk  rdk  st	        d«      ‚ t	        d«      ‚| j                  |«      }|d   |d   }}| j                  5  | j                  st	        d«      ‚t        |«      t        d«      z
  }|t        | j                  «      k\  r0t	        d	|› d
t        dt        | j                  «      z   «      › �«      ‚|| j                  v rJ| j                  j                  |«      }| j                  |   }	||	c| j                  |<   | j                  |<   n|| j                  |<   t        | j                  «      }
ddd«       | j                  t        dd
i«      «       d||||
dœS # 1 sw Y   Œ.xY w)zMPut a racing boat in a named slot, swapping with its old slot when necessary.r'   r(   ÚAÚZz&slot must be one letter, for example Br4   r$   z0no race with camera slots is currently publishedzslot z' is unavailable; available slots are A-é@   NÚcamera_slotsr:   Úassigned)r1  rY  r4   r$   r:   )r.   r˜   Úupperr§   r-   r[  r  rÂ  ÚordrØ  ÚindexrÖ  Ú_fan_outr    )r|   rY  Úboat_refÚ	slot_nameÚresolvedÚ	boat_textÚ	device_idrâ  Ú	old_indexÚ	displacedrG   s              r   r\  z!LiveFeedServer.assign_camera_slotµ  s³  € ä˜š
 “O×)Ñ)Ó+×1Ñ1Ó3ˆ	Üˆy‹>˜QÒ s¨iÔ'>¸3Ò'>ÜÐEÓFÐFð (?ÜÐEÓFÐFØ×+Ñ+¨HÓ5ˆØ'¨Ñ/°¸*Ñ1E�9ˆ	Ø�Z‰Zñ 	-Ø×%Ò%Ü Ð!SÓTÐTÜ˜	“N¤S¨£XÑ-ˆEØœ˜D×.Ñ.Ó/Ò/Ü  5¨¨ð 4&Ü&)¨"¬s°4×3EÑ3EÓ/FÑ*FÓ&GÐ%Hð"Jó Kð Kà˜D×.Ñ.Ñ.Ø ×.Ñ.×4Ñ4°YÓ?�	Ø ×.Ñ.¨uÑ5�	ØKTÐV_ÐH�×"Ñ" 5Ñ)¨4×+=Ñ+=¸iÒ+Hð -6�×"Ñ" 5Ñ)Ü˜×+Ñ+Ó,ˆE÷	-ð  	�‰”d˜>¨M¸5Ð+AÓBÔCØ$¨iÀØ%°eñ=ð 	=÷#	-ð 	-ús   ÂC3F#Æ#F,c           	     ó–  — t        |xs d«      j                  «       }| j                  5  t        | j                  «      }ddd«       g }D ]~  }|j                  d«      xs g D ]d  }|j                  d«      rŒ|t        |j                  d«      xs d«      t        |j                  d«      xs d«      fv sŒT|j                  |«       Œf Œ€ |D �ch c]%  }|j                  d«      sŒ|j                  d«      ’Œ' }}t        |«      dk7  r|sdnd	}t        d
|›d|› d�«      ‚|t        t        |«      «      dœS # 1 sw Y   ŒûxY wc c}w )zOResolve an operator's sail number or device ID against the current race roster.r'   Nr#   r%   r$   r&   r(   z	not foundÚ	ambiguouszboat z is z in the current race)r4   r$   )r.   r˜   r  rÖ  rÁ  r)   r*   r§   r-   ÚnextÚiter)	r|   rä  rç  r0   Úmatchesr3   r4   Ú
device_idsr¨  s	            r   r[  z"LiveFeedServer.resolve_camera_boatÐ  s8  € ä˜š BÓ'×-Ñ-Ó/ˆ	Ø�Z‰Zñ 	-Ü˜×+Ñ+Ó,ˆE÷	-àˆØò 	)ˆDØŸ™ Ó)Ò/¨Rò )�Ø—8‘8˜KÔ(ØØ¤ T§X¡X¨jÓ%9Ò%?¸RÓ!@Ä#ÀdÇhÁhÈxÓFXÒF^Ð\^ÓB_Ð `Ò`Ø—N‘N 4Õ(ñ	)ð	)ð 8?ÖW¨tÀ$Ç(Á(È:ÕBV�d—h‘h˜zÕ*ÐWˆ
ÐWÜˆz‹?˜aÒÙ(2‘[¸ˆFÜ˜u Y M°°f°XÐ=QÐRÓSÐSØ!¬t´D¸Ó4DÓ/EÑFÐF÷	-ð 	-üò Xs   ªD:ÃEÃ(EÄ:Ec                ó    — | j                   d   S )Nr(   )Úserver_addressrG  s    r   rË  zLiveFeedServer.portâ  s   € à×"Ñ" 1Ñ%Ð%r!   c                óp   — | j                   5  t        | j                  «      cddd«       S # 1 sw Y   yxY w)z·Open SSE subscribers. Distinct from the renderer registry: a socket can be open while the
        page behind it has stopped drawing, and that gap is exactly what the registry closes.N)r  r§   r½  rG  s    r   rå   zLiveFeedServer.client_countæ  s,   € ð �Z‰Zñ 	&Ü�t—}‘}Ó%÷	&÷ 	&ò 	&úó   �,¬5c                ó‚   •— t        j                  «       d   }t        |t        t        f«      ryt
        ‰| �  ||«       y)uÁ   Stay quiet on client disconnects (browser/OBS reload, EventSource reconnect) â€” these raise
        ConnectionError/TimeoutError while reading or writing the socket and are normal, not faults.r(   N)rœ  Úexc_infor=   rj  r  ry   Úhandle_error)r|   r¹   Úclient_addressr'  r   s       €r   r÷  zLiveFeedServer.handle_errorì  s6   ø€ ô �l‰l‹n˜QÑˆÜ�cœO¬\Ð:Ô;ØÜ‰Ñ˜W nÕ5r!   c                ó¨   — t        j                  «       }| j                  5  | j                  j	                  |«       d d d «       |S # 1 sw Y   |S xY wr�   )rh  ÚQueuer  r½  Úadd©r|   rl  s     r   rb  zLiveFeedServer._subscribeõ  s@   € ÜŸ™›ˆØ�Z‰Zñ 	!Ø�M‰M×Ñ˜aÔ ÷	!àˆ÷	!àˆús   ¡AÁAc                ó|   — | j                   5  | j                  j                  |«       d d d «       y # 1 sw Y   y xY wr�   )r  r½  Údiscardrü  s     r   rk  zLiveFeedServer._unsubscribeû  s0   € Ø�Z‰Zñ 	%Ø�M‰M×!Ñ! !Ô$÷	%÷ 	%ñ 	%ús   �2²;c                óp   — | j                   5  t        | j                  «      cd d d «       S # 1 sw Y   y xY wr�   )r  rÖ  r¾  rG  s    r   rd  zLiveFeedServer._backlogÿ  s*   € Ø�Z‰Zñ 	'Ü˜Ÿ™Ó&÷	'÷ 	'ò 	'úrô  F©Úresetc               ó–  — | j                   5  |r	|g| _        np| j                  j                  |«       t        | j                  «      | j                  dz   kD  r0| j                  d   g| j                  | j                   d  z   | _        t        | j                  «      }d d d «       D ]  }|j                  |«       Œ y # 1 sw Y   Œ"xY w)Nr(   r   )r  r¾  r*   r§   rÀ  rÖ  r½  Úput)r|   r±   r  rÙ   rl  s        r   rã  zLiveFeedServer._fan_out  s¯   € Ø�Z‰Zñ 	*ÙØ!& �•à—‘×$Ñ$ UÔ+Ü�t—}‘}Ó%¨×(9Ñ(9¸AÑ(=Ò=Ø%)§]¡]°1Ñ%5Ð$6¸¿¹È×HYÑHYÐGYÐGZÐ9[Ñ$[�D”MÜ˜4Ÿ=™=Ó)ˆG÷	*ð ò 	ˆAØ�E‰E�%�Lñ	÷	*ð 	*ús   �BB?Â?Cc                óŽ   — |r3t        j                  | j                  d¬«      }|j                  «        |S | j                  «        y)zTBegin serving. background=True returns immediately on a daemon thread; False blocks.T)ÚtargetÚdaemonN)r  ÚThreadÚserve_foreverrT   )r|   Ú
backgroundÚts      r   rT   zLiveFeedServer.start  s:   € áÜ× Ñ ¨×(:Ñ(:À4ÔHˆAØ�G‰GŒIØˆHØ×ÑÔØr!   c                óØ  — t        |||«      }t        t        j                  j	                  t
        «      xs dj                  «       xr5 t        j                  j	                  t        «      xs dj                  «       «      }|ri |¥ddi¥}| j                  |«      }t        |j	                  d«      xs g «      }	t        |xs i j                  «       «      }
|
j                  |	«       |
j                  |j	                  d«      xs d«       |
j                  d„ |j	                  d«      xs i j                  «       D «       «       | j                  5  || j                  k7  s!t        |	«      t        | j                   «      k7  r|	| _        || _        |xs g | _        |r+|j	                  d	«      rt%        |j	                  d	«      «      nd
| _        | j&                  r|
n	t        «       | _        |j	                  d	«      |j	                  d«      dœ| _        t-        |j	                  d«      t.        «      r|j	                  d«      ni }|j	                  d«      |j	                  d«      |j	                  d«      dœ| _        | j                   ri |¥dt        | j                   «      i¥}d
d
d
«       | j3                  t5        d||||xs i |xs i |xs g dœ«      d¬«       y
# 1 sw Y   Œ:xY w)uŸ  Send the static race header to clients (and reset the backlog for a new race). `media`/`video`
        carry the per-boat tile pictures/clips so the renderer's boat images show in live mode too.

        The header is COMPLETED here, not by its callers: `meta.cameraSlots` is derived from the very
        arguments this method already holds. There are three publishers today (`serve_v3_live`, the
        simulator's `bridge_3d`, `race_day`) and the field they need is a pure function of the header â€”
        so deriving it once on the way to the wire is the only arrangement in which they cannot
        disagree. A caller that computes its own is honoured unchanged.r'   ÚcameraPlaylistProxyTr:   r;   r<   c              3  óD   K  — | ]  }t        |t        «      r|r|–— Œ y ­wr�   )r=   r.   rû   s     r   rý   z.LiveFeedServer.publish_init.<locals>.<genexpr>/  s%   è ø€ ò  E¨%Ü#-¨e´SÔ#9¹eô !&ñ  Eùs   ‚ ÚvesselCamerasrÐ  NÚraceName)rÐ  r  ÚvenueÚ	eventNameÚtimeZoneÚsanctionMark)r  ÚvenueTimeZoner  Úinit)rA   Úcourser0   ÚparamsrB   Úvideor   )rH   rÌ   ræ   rç   r)   r  r˜   r	  rÒ  rÖ  r@   ÚkeysÚupdateÚvaluesr  rÃ  rÂ  rÁ  r.   r  r  rÇ  r=   r>   r.  rã  r    )r|   rA   r  r0   r  rB   r  Úproxy_readyÚidentityÚderived_slotsÚplaylist_camerasr  s               r   Úpublish_initzLiveFeedServer.publish_init  sœ  € ô !  u¨eÓ4ˆÜœBŸJ™JŸN™NÔ+BÓCÒIÀr×PÑPÓRò XÜ "§
¡
§¡Ô/GÓ HÒ NÈB×UÑUÓWóYˆáð 9�dÐ8Ð1°4Ñ8ˆDØ×&Ñ& tÓ,ˆÜ˜TŸX™X mÓ4Ò:¸Ó;ˆÜ ¢¨×1Ñ1Ó3Ó4ÐØ×Ñ Ô.Ø×Ñ §¡Ð)HÓ IÒ OÈRÔPØ×Ññ  E°D·H±H¸_Ó4MÒ4SÐQS×3[Ñ3[Ó3]ô  Eô 	Eà�Z‰Zñ 	IØ˜4×5Ñ5Ò5¼¸]Ó9KÌsÐSW×SeÑSeÓOfÒ9fØ%2�Ô"Ø)1ˆDÔ&Ø!&¢¨"ˆDÔá*5¸$¿(¹(À<Ô:Pô (+¨4¯8©8°LÓ+AÔ'BØVZð Ô#à9=×9PÒ9PÑ%5ÔVYÓV[ˆDÔ"à"Ÿh™h |Ó4À$Ç(Á(È:ÓBVñ)XˆDÔ%ä)3°D·H±H¸WÓ4EÄtÔ)L�D—H‘H˜WÔ%ÐRTˆEà!ŸX™X kÓ2ð "'§¡¨:Ó!6ð !%§¡¨Ó 8ñ	(ˆDÔ$ð ×!Ò!ØH˜$ÐH ¬t°D×4FÑ4FÓ/GÑH�÷-	Ið. 	�‰”d˜6¨D¸FÈUØ.4ªl¸ÀUÂ[ÈbÐ[`Ò[fÐdfñ$hó ià ð 	õ 	"÷/	Ið 	Iús   ÅEK Ë K)c
           	     ój  — | j                   5  t        | j                  «      }
ddd«       | j                  «       }|xs g D �]  }t	        j
                  |
j                  d«      |
j                  d«      |j                  d«      |¬«      }|€ŒN| j                   5  |d   }| j                  j                  |d«       || j                  |<   t        | j                  «      t        kD  rT| j                  j                  t        t        | j                  «      «      «       t        | j                  «      t        kD  rŒTddd«       | j                  |«       �Œ |||||xs g dœ}|�||d<   |r||d	<   |r||d
<   |	r|	|d<   | j                  t        d|«      «       y# 1 sw Y   �Œ{xY w# 1 sw Y   ŒnxY w)uŠ  Push one tick: live frontier positions + scored standings/events (from StreamingScorer).
        `ais` is the optional AIS context-vessel overlay for this tick (within ~500 m of the boat â€”
        an overlay, never scored); omitted/empty when no AIS feed is wired. `program` is the explicit
        presentation phase from `program_state()`; included on the wire whenever the driver computes
        it, so clients can't mistake a stale result for a live race. `marks` (role â†’ {lat,lon}) is the
        optional set of course marks/RC-vessels that are MOVING this tick â€” the renderer glides them to
        the new station (e.g. the committee re-setting the course to a shifted wind between races).
        `wind` ({fromDeg, kts}) is the optional LIVE wind for this tick â€” the renderer re-points the
        on-water wind field + sea state (and thus the RC vessels weathervane) as the breeze veers.NrÐ  r  ÚtSec)r¹  r  Ú	race_nameÚ
source_secÚ
commentaryÚcommentaryVariantId)rr   r/  Ú	positionsÚeventsÚaisÚprogramÚmarksÚwindÚrieOnboardDirectivesÚdelta)r  r>   rÇ  rÆ  Ú	asrun_logÚcommentary_selected_rowr)   rÈ  rM  r§   Ú_COMMENTARY_VARIANT_BACKLOGrí  rî  Ú_record_commentary_rowrã  r    )r|   rr   r/  r(  r'  r)  r*  r+  r,  ÚonboardÚcommentary_identityÚselected_atr   ÚrowÚ
variant_idÚpayloads                   r   Úpublish_deltazLiveFeedServer.publish_deltaL  sª  € ð �Z‰Zñ 	BÜ"& t×'@Ñ'@Ó"AÐ÷	Bà—l‘l“nˆØ’\˜ró 	1ˆEÜ×3Ñ3Ø"Ð0C×0GÑ0GÈÓ0UØ-×1Ñ1°*Ó=È%Ï)É)ÐTZÓJ[Ø ô"ˆCð ‰Ø—Z‘Zñ ]Ø!$Ð%:Ñ!;�JØ×-Ñ-×1Ñ1°*¸dÔCØ<?�D×-Ñ-¨jÑ9Ü˜d×7Ñ7Ó8Ô;VÒVØ×1Ñ1×5Ñ5´d¼4À×@YÑ@YÓ;ZÓ6[Ô\ô ˜d×7Ñ7Ó8Ô;VÓV÷	]ð ×+Ñ+¨CÖ0ð	1ð ¨IÀIØ#¨CªI°2ñ7ˆàÐØ!(ˆG�IÑÙØ$ˆG�GÑÙØ"ˆG�F‰OÙØ.5ˆGÐ*Ñ+Ø�‰”d˜7 GÓ,Õ-÷5	Bñ 	Bú÷]ð ]ús   �FÂB F)ÆF&Æ)F2	c                óÌ  ‡‡— t        j                  | j                  «       |¬«      Š‰€y| j                  5  | j                  j                  ‰d   «      Š‰€
	 ddd«       yd}t        ˆˆfd„|D «       «      r
	 ddd«       y‰d   ‰d   f}|| j                  v r
	 ddd«       yd| j                  |<   t        | j                  «      t        kD  rT| j                  j                  t        t        | j                  «      «      «       t        | j                  «      t        kD  rŒTd	D ]  }‰j                  |«      ‰|<   Œ 	 ddd«       | j                  ‰«       y# 1 sw Y   ŒxY w)
zKRecord the authoritative server-time receipt for an audible renderer start.)r¹  r8  NFr&  )ÚcommentaryVariantCategoryÚcommentaryVariantIndexÚcommentaryVariantCountr&  Ú	sourceSecc              3  ód   •K  — | ]'  }‰j                  |«      ‰j                  |«      k7  –— Œ) y ­wr�   rÎ  )rü   rÏ  r6  Úselecteds     €€r   rý   z<LiveFeedServer.record_commentary_playback.<locals>.<genexpr>‚  s'   øè ø€ ÒG¸�3—7‘7˜3“< 8§<¡<°Ó#4Õ4ÑGùs   ƒ-0Ú
rendererIdT)rÐ  r  r>  )r/  Úcommentary_started_rowrÆ  r  rÈ  r)   ÚanyrÉ  r§   Ú_COMMENTARY_RECEIPT_BACKLOGrM  rí  rî  r2  )r|   r8  ÚstableÚreceiptrÏ  r6  r@  s        @@r   rÏ   z)LiveFeedServer.record_commentary_playbackw  sY  ù€ ä×.Ñ.°d·l±l³nÈgÔVˆØˆ;ØØ�Z‰Zñ 	-Ø×0Ñ0×4Ñ4°SÐ9NÑ5OÓPˆHØÐØ÷	-ð 	-ðTˆFäÔGÀÔGÔGØ÷	-ð 	-ð ˜<Ñ(¨#Ð.CÑ*DÐEˆGØ˜$×3Ñ3Ñ3Ø÷	-ð 	-ð 26ˆD×%Ñ% gÑ.Ü�d×/Ñ/Ó0Ô3NÒNØ×)Ñ)×-Ñ-¬d´4¸×8QÑ8QÓ3RÓ.SÔTô �d×/Ñ/Ó0Ô3NÓNð ?ò -�Ø#Ÿ<™<¨Ó,��C’ñ-÷!	-ð$ 	×#Ñ# CÔ(Ø÷'	-ð 	-ús$   ·"EÁ"EÂEÂ%A>EÄ$EÅE#c                óÐ   — t        j                  | j                  || j                  ¬«       	 | j                  dt	        j
                  |dd¬«      z   «       y# t        $ r Y yw xY w)zSPersist when configured and always leave one queryable structured runtime log line.)r¸  z[commentary-asrun] F)ú,ú:)r   Ú
separatorsN)r/  r*   rÄ  rÅ  r   r   rY   )r|   r6  s     r   r2  z%LiveFeedServer._record_commentary_row‘  s^   € ä×Ñ˜×)Ñ)¨3°t·|±|ÕDð	Ø�L‰LÐ.´·±¸CÈeØGQô2Sñ Sõ Tøäò 	Ùð	ús   ®*A Á	A%Á$A%r'   r©  )ÚreasonÚsince_sc               óz   — | j                  t        dt        |«      |t        t	        |«      d«      dœ«      «       y)u2  The producer is ON AIR but has published no tick for a while â€” or has recovered.

        A separate event, not a field on `delta`, because a delta means "here is a scored second" and
        the whole point of this signal is that there is no second to score. Sent on the transition
        AND re-sent while the stall lasts, so it stays inside the backlog tail a late joiner replays;
        `stale=False` on recovery is what clears it.

        This exists because the failure it reports is invisible otherwise: with no positions the
        producer published nothing at all, the renderer held its last frame, and a frozen scene under
        a green LIVE badge is indistinguishable from a becalmed fleet. Seen live on 2026-08-02, where
        the broadcast froze early and stayed frozen for the rest of the day.r$  r(   )r$  rK  ÚsinceSN)rã  r    rÌ   rp   r°  )r|   r$  rK  rL  s       r   Úpublish_stalezLiveFeedServer.publish_staleš  s7   € ð 	�‰”d˜7¬d°5«kÀVÜ/4´U¸7³^ÀQÓ/Gñ%Ió Jõ 	Kr!   c                ó:   — | j                  t        d|«      «       y)zVPublish presentation-result progress (winner, then subsequent finishers/coverage end).ri   N©rã  r    )r|   ri   s     r   Úpublish_resultzLiveFeedServer.publish_result©  s   € à�‰”d˜8 VÓ,Õ-r!   c                ó:   — | j                  t        d|«      «       y)uÛ  A moment worth a replay â€” offered, never taken.

        The producer says what happened and when it was VISIBLE; something at the recording end (the
        A4000's `replay_take`) may put it on air, and only because a person pressed it. Nothing here
        is an instruction and nothing reads a reply: the broadcast presents, it does not decide, and a
        producer that could cut the picture would be deciding.

        The instant is PRESENTATION time, not water time. The recording holds the picture as it went
        out, so a cue carrying the moment the boats rounded would cut a replay one presentation delay
        early â€” the whole clip in the wrong place, and plausible enough on screen to be believed.
        Ú
replay_cueNrQ  )r|   Úcues     r   Úpublish_replay_cuez!LiveFeedServer.publish_replay_cue­  s   € ð 	�‰”d˜<¨Ó-Õ.r!   c                ó:   — | j                  t        d|«      «       y)zTPublish the committee start-camera's race identity without changing the on-air race.Ústart_cameraNrQ  )r|   r1  s     r   Úpublish_start_cameraz#LiveFeedServer.publish_start_camera»  s   € à�‰”d˜>¨6Ó2Õ3r!   c                óà   — | j                   5  t        | j                  «      }ddd«       D ]  }|j                  d«       Œ | j	                  «        | j                  «        y# 1 sw Y   ŒBxY w)z0Unblock every streaming client and stop serving.N)r  rÖ  r½  r  ÚshutdownÚserver_close)r|   rÙ   rl  s      r   ÚstopzLiveFeedServer.stop¿  s[   € à�Z‰Zñ 	*Ü˜4Ÿ=™=Ó)ˆG÷	*àò 	ˆAØ�E‰E�$�Kð	à�‰ŒØ×ÑÕ÷	*ð 	*ús   �A$Á$A-)z0.0.0.0i="  )
rÊ  r.   rË  r+   r¶  ú
int | Noner·  zPath | Noner/  rs  )rA   r>   ro  rÑ  )ro  r>   )rY  Úobjecträ  r_  ro  r>   )rä  r_  ro  r>   )ro  r+   )ro  úqueue.Queue)rl  r`  )ro  zlist[bytes])r±   r’   r  rÌ   )T)r	  rÌ   ro  zthreading.Thread | None)NNN)rA   r>   r  r>   r0   rÖ  r  rs  rB   rs  r  úlist | None)NNNNN)rr   zfloat | intr/  rs  r(  rÖ  r'  r>   r)  ra  r*  rs  r+  rs  r,  rs  r3  ra  )r8  r_  ro  rÌ   )r6  r>   ro  rp  )r$  rÌ   rK  r.   rL  r°  )ri   r>   )rU  r>   )r1  r>   )"rt  ru  rv  rw  Údaemon_threadsÚallow_reuse_addressr¿  ÚLIVE_BACKLOGÚprintrz   ÚstaticmethodrÒ  rZ  r\  r[  r²  rË  rå   r÷  rb  rk  rd  rã  rT   r   r9  rÏ   r2  rO  rR  rV  rY  r]  rx  ry  s   @r   r´  r´  e  s7  ø„ ñið €NØÐð €Kð €Lð,,Ð^bØ+/¸ÀdØ*.ñ,,Ø(ð,,à'õ,,ð\ ò]ó ð]óTó=ó6Gð$ ò&ó ð&ó&ô6óó%ó'ð 7<õ 
ôð Y]ØEIð1"Ø'ð1"Ø7Bó1"ðh GKØEIØ-1ð).Ø&ð).Ø8Cð).à(ð).à7Bð).ð  +ó).óVó4ð ;=Èsõ Kó.ó/ó4ör!   r´  )r   r.   r   r>   ro  r’   r�   )r0   rÖ  ro  z	list[str])rA   r>   r0   rÖ  rB   rs  ro  r>   )rI   NN)
rZ   r°  r[   r+   r\   ra  r]   r^  ro  rq  )r]   r+   rq   r+   rr   r+   rs   rÌ   r[   r+   r\   ra  ro  r>   )r�  rs  ro  r>   )Nrw  Ú
__future__r   r>  r¨   r   ræ   rh  Úrerœ  r  r  Úhttp.serverr   r   Úpathlibr   Úurllib.errorr   r   Úurllib.parser	   r
   r   r   Úurllib.requestr   r   r/  Úbroadcast_controlr   Úrenderer_registryr   Úcompiler–   r_  r†   r™  rš  Úparentr{   ró   rÐ   rÍ   r1  rD  rÁ   rÂ   rÃ   ré   rä   rÄ   rg  rc  rï   rð   râ   r  r	  r  r  r  rí   r±  r~  r  r€  r    r8   rH   rf   Ú	frozensetrU   rt   rv   r‡  r‰  r´  r<   r!   r   ú<module>rs     sá  ðñõ8 #ã Û 	Û Û 	Û Û 	Û 
Û Û ß EÝ ß ,ß ;Ó ;ß +ó
 Ý -Ý .ð ˆB�J‰JÐ-Ó.€	ñ ‹H€	á�x“.×(Ñ(Ó*×1Ñ1×8Ñ8€Ø€ð %Ð ð 3Ð Ø"Ð Ø"Ð ð /Ð Ø4Ð Ø2Ð Ø€ð '€àÐ ð €ð Ð ð 3Ð Ø%¨Ñ.°Ñ6Ð9KÑKÐ ð
 !�b—j‘jÐ!SÓTÐ Ø1Ð Ø3Ð Ø-Ð ð Ð Ø$(Ð !ð $€ð Ð à'Ð Ø-Ð Ø2Ð óXô
'ó>=ð( W[Ø/3ð .Ø",ð .Ø8Bó .ñJ ÒOÓPÐ ð &)ÀñUØ"ðUØ3>ðUØJNóUô<Y Ð'ô Y ôx÷,Yñ YôxaÐ(õ ar!   