a
    ã¬•jH  ã                   @   s0  d 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
Z
ddlmZmZ dZdddddœZd	Zd
Zdd„ ZdZeƒ \ZZdd„ ZdZdZdd„ Zefdd„Zdd„ ZdZdd„ ZdddddœZe	 ¡ Z dd„ Z!d d!„ Z"d"Z#d#d$„ Z$G d%d&„ d&eƒZ%d'd(„ Z&e'd)k�r,e (e&ƒ ¡ dS )*a  Serve the dashboard on the local network. No publishing, no upload.

WHY. The GitHub Pages route republished a 9MB encrypted page every 30 minutes,
which accumulated to a 2.2GB repository and got the account suspended for
excessive bandwidth. The fix is not a better host -- it is to stop uploading.
The page already exists on this machine; a phone can just fetch it.

WHAT IT DOES NOT SERVE. A bare `python -m http.server` over E:\soccer would
expose .env (Kalshi keys, Poly-US keys, Gmail password, GitHub token) to
anything on the LAN. This serves an EXPLICIT ALLOW-LIST of files and nothing
else -- no directory listing, no path traversal, no wildcards.

PLAINTEXT ON PURPOSE. dashboard.html on disk is unencrypted; only the published
copy was ever AES-wrapped, and that was solely because Pages is world-readable.
On your own network there is nothing to hide it from, which also means the page
compresses normally instead of being an incompressible blob.

Usage:
    python main\serve_dashboard.py               -> http://<this-box>:8000/
    python main\serve_dashboard.py --port 9000
    python main\serve_dashboard.py --localhost   -> 127.0.0.1 only

Reachable from the phone on the same wifi at http://<box-ip>:8000/.
Away from home, put Tailscale on both and use the box's Tailscale name.
é    N)ÚBaseHTTPRequestHandlerÚThreadingHTTPServerz	E:\soccer)zdashboard.htmlútext/html; charset=utf-8)zresearch\mm_book_dataset.csvútext/csv)zresearch\mm_live_realfills.csvr   )ú/ú/dashboard.htmlz	/book.csvz
/fills.csvz"E:/soccer/data/dashboard_auth.jsonzsoccer dashboardc               
   C   s¸  zÜt tdd��} t | ¡}W d  ƒ n1 s.0    Y  | d¡rÚ| d¡rÚ| d¡sºt d¡|d< zBt tddd�� } tj|| d	d
� W d  ƒ n1 sš0    Y  W n ty¸   Y n0 |d t	ƒ d< |d |d fW S W n tt
fyò   Y n0 dt d¡t d¡dœ}|d t	ƒ d< zRt tddd�� } tj|| d	d
� W d  ƒ n1 �sR0    Y  tdt› �ƒ W n: t�y¦ } z tdt› d|› �ƒ W Y d}~n
d}~0 0 |d |d fS )aŸ  Read the credentials, creating strong ones on first run.

    Kept out of .env deliberately: .env holds exchange keys, and this file is
    something you will want to open and read off to type into a browser.

    `key` is a THIRD credential, for a browser that cannot be typed into at all.
    A managed work machine suppressed the basic-auth dialog AND blocked text
    input to the login form (2026-08-31), leaving no way to present a password.
    The key travels in the URL instead and is swapped for a cookie on arrival.
    It is deliberately separate from the password so it can be rotated on its
    own after being pasted somewhere it should not have been.
    úutf-8)ÚencodingNÚuserÚpasswordÚkeyé   Úwé   )ÚindentÚ
ACCESS_KEYZsocceré   )r
   r   r   zwrote new credentials to zcould not write ú: )ÚopenÚ	AUTH_FILEÚjsonÚloadÚgetÚsecretsZtoken_urlsafeÚdumpÚOSErrorÚglobalsÚ
ValueErrorÚprint)ÚfhÚaÚe© r"   úmain/serve_dashboard.pyÚ	load_authA   s4    (
2
ÿ0*r$   Ú c                 C   st   | r|   d¡sdS zt | dd… ¡ d¡}W n tyB   Y dS 0 | d¡\}}}t |t¡}t |t	¡}|or|S )zFConstant-time check of an Authorization header. False if absent/wrong.zBasic Fé   Nr   ú:)
Ú
startswithÚbase64Z	b64decodeÚdecodeÚ	ExceptionÚ	partitionÚhmacÚcompare_digestÚUSERÚPASSWORD)ÚheaderÚrawr
   Ú_ÚpwÚok_uÚok_pr"   r"   r#   Ú
check_authm   s    r7   ZdashiÐ  c                   C   s   t d t  d¡S )Nr'   r   )r/   r0   Úencoder"   r"   r"   r#   Ú_secretŒ   s    r9   c                 C   sF   t tt ¡ | d  ƒƒ}t tƒ | ¡ tj¡ 	¡ d d… }|d | S )Né  é    Ú.)
ÚstrÚintÚtimer-   Únewr9   r8   ÚhashlibÚsha256Ú	hexdigest)ZhoursÚexpÚmacr"   r"   r#   Ú
make_token�   s    "rF   c                 C   sn   zT| pd  dd¡\}}t tƒ | ¡ tj¡ ¡ dd… }t ||¡oRt	|ƒt
 
¡ kW S  tyh   Y dS 0 dS )z²Valid, unexpired, and signed by the current password.

    Signing with the password means rotating it invalidates every session, which
    is what you want from a rotation.
    r%   r<   é   Nr;   F)Úsplitr-   r@   r9   r8   rA   rB   rC   r.   r>   r?   r+   )ÚtokrD   rE   Zwantr"   r"   r#   Útoken_ok–   s    "rJ   a9  <!doctype html><meta name=viewport content="width=device-width,initial-scale=1">
<title>soccer dashboard</title>
<style>
 :root{color-scheme:light dark}
 body{font:16px/1.5 system-ui,-apple-system,sans-serif;display:flex;min-height:90vh;
      align-items:center;justify-content:center;margin:0}
 form{width:min(320px,86vw);display:flex;flex-direction:column;gap:10px}
 h1{font-size:17px;margin:0 0 6px;font-weight:600}
 input{font:16px system-ui;padding:11px 12px;border-radius:9px;
       border:1px solid rgba(127,127,127,.45);background:transparent;color:inherit}
 button{font:600 16px system-ui;padding:12px;border:0;border-radius:9px;
        background:#1f6feb;color:#fff;cursor:pointer}
 .err{color:#e5534b;font-size:13px;min-height:18px}
</style>
<form method=post action="/login">
  <h1>soccer dashboard</h1>
  <input name=u placeholder="username" autocomplete="username" autocapitalize=off autocorrect=off>
  <input name=p type=password placeholder="password" autocomplete="current-password">
  <button type=submit>Sign in</button>
  <div class=err>__ERR__</div>
</form>c                 C   s@   ddl m} ||  dd¡ƒ}| ddg¡d | ddg¡d fS )Nr   ©Úparse_qsr   ÚreplaceÚur%   Úp)Úurllib.parserL   r*   r   )ÚbodyrL   Úqr"   r"   r#   Ú_form_fields»   s    rS   Zidleç        ©ÚstateZstartedÚfinishedÚmessagec               
   C   s0  t   ¡ } zdtjtjtj tdd¡gtdddddd�}|j	dk}|j
pFd	|jpNd	  ¡  ¡ }|rf|d
 nd	}W n> tyª } z&dt|ƒj› d|› � }}W Y d }~n
d }~0 0 t�< |rºdndtd< t   ¡ td< |d d… td< W d   ƒ n1 sò0    Y  tdtd › dt   ¡ |  d›d|d d… › �ƒ d S )NÚmainzbuild_dashboard.pyTr   rM   iX  )ÚcwdZcapture_outputÚtextr	   ÚerrorsZtimeoutr   r%   éÿÿÿÿFr   ÚokÚerrorrV   rW   i,  rX   z	refresh: z in z.1fzs éx   )r?   Ú
subprocessÚrunÚsysÚ
executableÚosÚpathÚjoinÚROOTÚ
returncodeÚstdoutÚstderrÚstripÚ
splitlinesr+   ÚtypeÚ__name__Ú_BUILD_LOCKÚBUILDr   )Zt0rO   r^   ÚtailÚmsgr!   r"   r"   r#   Ú	_do_buildÔ   s$    ÿý
0.rt   c                  C   s|   t �R td dkr(ttƒW  d  ƒ S t dt ¡ dddœ¡ ttƒ} W d  ƒ n1 s\0    Y  tjtdd� ¡  | S )	zDKick off a rebuild unless one is already running. Returns the state.rV   ZrunningNrT   r%   rU   T)ÚtargetZdaemon)	rp   rq   ÚdictÚupdater?   Ú	threadingZThreadrt   Ústart)Zsnapshotr"   r"   r#   Ústart_buildç   s    ÿ&rz   uQ  
<style>
  #rfBtn{position:fixed;right:16px;bottom:16px;z-index:99999;font:600 14px/1
    system-ui,-apple-system,sans-serif;padding:12px 18px;border-radius:999px;
    border:1px solid rgba(127,127,127,.35);cursor:pointer;
    background:#1f6feb;color:#fff;box-shadow:0 2px 12px rgba(0,0,0,.3);
    -webkit-tap-highlight-color:transparent}
  #rfBtn[disabled]{opacity:.65;cursor:default}
  #rfMsg{position:fixed;right:16px;bottom:64px;z-index:99999;max-width:60vw;
    font:500 12px/1.35 system-ui,-apple-system,sans-serif;padding:8px 12px;
    border-radius:10px;background:rgba(0,0,0,.82);color:#fff;display:none}
</style>
<button id="rfBtn">Refresh</button><div id="rfMsg"></div>
<script>
(function(){
  var b=document.getElementById('rfBtn'), m=document.getElementById('rfMsg'), t0;
  function say(s){ m.style.display=s?'block':'none'; m.textContent=s||''; }
  function poll(){
    fetch('/refresh/status',{cache:'no-store'}).then(function(r){return r.json();})
      .then(function(j){
        if(j.state==='running'){
          say('Rebuildingâ€¦ '+Math.round((Date.now()-t0)/1000)+'s');
          return setTimeout(poll,1500);
        }
        if(j.state==='ok'){ say('Updated, reloadingâ€¦');
          return setTimeout(function(){ location.reload(); },400); }
        say('Failed: '+(j.message||'unknown')); b.disabled=false;
        b.textContent='Refresh';
      })
      .catch(function(e){ say('Lost the server: '+e); b.disabled=false;
                          b.textContent='Refresh'; });
  }
  b.addEventListener('click',function(){
    b.disabled=true; b.textContent='Rebuilding'; t0=Date.now(); say('Startingâ€¦');
    fetch('/refresh',{method:'POST',cache:'no-store'})
      .then(function(){ poll(); })
      .catch(function(e){ say('Could not start: '+e); b.disabled=false;
                          b.textContent='Refresh'; });
  });
})();
</script>
c                 C   sD   d}t  d¡}|  |¡}|dkr<| d|… | | |d…  S | | S )z:Put the refresh control into the page just before </body>.s   </body>r   r]   N)Ú
REFRESH_UIr8   Úrfind)rQ   ÚtagZuiÚir"   r"   r#   Úinject"  s    

r   c                   @   sP   e Zd Zdd„ Zdd„ Zddd„Zdd	„ Zddd„Zdd„ Zdd„ Z	dd„ Z
dS )ÚHandlerc                 C   sZ   d}|   d¡ |  ddt› d�¡ |  dd¡ |  dtt|ƒƒ¡ |  ¡  | j |¡ d S )	Ns   authentication requiredi‘  zWWW-AuthenticatezBasic realm="z", charset="UTF-8"úContent-Typeztext/plain; charset=utf-8úContent-Length)Úsend_responseÚsend_headerÚREALMr=   ÚlenÚend_headersÚwfileÚwrite)ÚselfrQ   r"   r"   r#   Ú_deny+  s    
zHandler._denyc                 C   s^   t | j d¡ƒrdS | j d¡p"d d¡D ].}| ¡  d¡\}}}|tkr*t|ƒr* dS q*dS )NZAuthorizationTZCookier%   ú;ú=F)r7   Úheadersr   rH   rl   r,   ÚCOOKIErJ   )rŠ   ÚpartÚkr3   Úvr"   r"   r#   Ú_authed4  s    zHandler._authedr%   c                 C   s`   t  d|¡ d¡}|  d¡ |  dd¡ |  dtt|ƒƒ¡ |  dd¡ |  ¡  | j 	|¡ d S )	NZ__ERR__r   éÈ   r�   r   r‚   úCache-Controlúno-store)
Ú
LOGIN_PAGErM   r8   rƒ   r„   r=   r†   r‡   rˆ   r‰   )rŠ   ÚerrrQ   r"   r"   r#   Ú_login_page=  s    
zHandler._login_pagec                 C   s   |   ¡ rdS |  ¡  dS )zoGate a DATA request. Never renders the form -- a CSV or a refresh
        POST wants a status code, not a page.TF)r“   r‹   )rŠ   r"   r"   r#   Ú_okF  s    zHandler._okr”   c                 C   s^   t  |¡ d¡}|  |¡ |  dd¡ |  dtt|ƒƒ¡ |  dd¡ |  ¡  | j 	|¡ d S )Nr   r�   zapplication/jsonr‚   r•   r–   )
r   Údumpsr8   rƒ   r„   r=   r†   r‡   rˆ   r‰   )rŠ   ÚobjÚcoderQ   r"   r"   r#   Ú_jsonN  s    
zHandler._jsonc              	   C   sü   | j  dd¡d }|dkrÈt| j d¡p*dƒ}t|r@| j |¡ndƒ\}}t 	|t
¡}t 	|t¡}|rº|rº|  d¡ |  dd	¡ |  d
t› dtƒ › dtd › d�¡ |  dd¡ |  ¡  n
|  d¡ d S |  ¡ sÔd S |dkrì|  dd¡ d S |  tƒ ¡ d S )Nú?rG   r   ú/loginr‚   ó    é/  ÚLocationr   ú
Set-Cookier�   ú; Path=/; Max-Age=r:   ú; HttpOnly; SameSite=LaxÚ0zwrong username or passwordz/refreshé”  ú	Not found)rf   rH   r>   rŽ   r   rS   ZrfileÚreadr-   r.   r/   r0   rƒ   r„   r�   rF   ÚSESSION_HOURSr‡   r™   rš   Ú
send_errorrž   rz   )rŠ   rf   ÚnrN   r4   r5   r6   r"   r"   r#   Údo_POSTW  s0    
ÿÿ

zHandler.do_POSTc              
   C   sF  | j  dd¡d }ddlm} d| j v r>|| j  dd¡d ƒni }| d¡pPdgd }|rÈtrÈt |t¡rÈ|  d¡ |  	d|d	kr†d
n|¡ |  	dt
› dtƒ › dtd › d�¡ |  	dd¡ |  ¡  d S |  ¡ sî|dv râ|  ¡  n|  ¡  d S |dk�r0t� |  ttƒ¡ W d   ƒ n1 �s"0    Y  d S t |¡}|�sP|  dd¡ d S |\}}tj  t|¡}z8t|dƒ�}	|	 ¡ }
W d   ƒ n1 �s’0    Y  W nD t�yâ } z*|  d|› dt|ƒj› �¡ W Y d }~d S d }~0 0 | d¡�røt |
ƒ}
|  d¡ |  	d|¡ |  	dt!t"|
ƒƒ¡ |  	dd¡ |  ¡  | j# $|
¡ d S )NrŸ   rG   r   rK   r‘   r%   r¢   r£   r    r   r¤   r�   r¥   r:   r¦   r‚   r§   )r   r   r    z/refresh/statusr¨   r©   Úrbi÷  z unavailable: z	text/htmlr”   r�   r•   r–   )%rf   rH   rP   rL   r   r   r-   r.   rƒ   r„   r�   rF   r«   r‡   r“   r™   r‹   rp   rž   rv   rq   ÚALLOWr¬   re   rg   rh   r   rª   r   rn   ro   r(   r   r=   r†   rˆ   r‰   )rŠ   rf   rL   rR   r‘   ÚentryZrelZctypeZfullr   rQ   r!   r"   r"   r#   Údo_GETp  sX    $
ÿÿ

.
,
zHandler.do_GETc                 G   s   t |  ¡ › d|| › �ƒ d S )Nú )r   Zaddress_string)rŠ   ZfmtÚargsr"   r"   r#   Úlog_message¤  s    zHandler.log_messageN)r%   )r”   )ro   Ú
__module__Ú__qualname__r‹   r“   r™   rš   rž   r®   r²   rµ   r"   r"   r"   r#   r€   *  s   		
	
	4r€   c               	   C   s@  d} dt jv r(tt jt j d¡d  ƒ} dt jv r6dnd}dd„ t ¡ D ƒ}|rjtd	d
 tt	|ƒƒ¡ ƒ t
|| ftƒ}tdttƒ› dt› d|› d| › �ƒ td| › d�ƒ |dk�r
dd l}z(| | ¡ ¡}td|› d| › d�ƒ W n tyø   Y n0 td| › d�ƒ tdƒ z| ¡  W n t�y:   tdƒ Y n0 dS )Ni@  z--portrG   z--localhostz	127.0.0.1z0.0.0.0c                 S   s*   g | ]"\}}t j t j t|¡¡s|‘qS r"   )re   rf   Úexistsrg   rh   )Ú.0Úrr3   r"   r"   r#   Ú
<listcomp>¯  s   ÿzmain.<locals>.<listcomp>znote: not yet on disk -> z, zserving z allow-listed paths from z on r'   z   this box   : http://localhost:r   r   z  same wifi  : http://z'  away       : http://<tailscale-name>:z/  (if Tailscale is on)zCtrl-C to stop.z
stopped)rc   Úargvr>   Úindexr°   Úvaluesr   rg   ÚsortedÚsetr   r€   r†   rh   ÚsocketZgethostbynameZgethostnamer   Zserve_foreverÚKeyboardInterrupt)ZportZhostZmissingZsrvrÁ   Zipr"   r"   r#   rY   ©  s0    
$
rY   Ú__main__))Ú__doc__r)   rA   r-   r   re   r   ra   rc   rx   r?   Zhttp.serverr   r   rh   r°   r   r…   r$   r   r/   r0   r7   r�   r«   r9   rF   rJ   r—   rS   rq   ZLockrp   rt   rz   r{   r   r€   rY   ro   Úexitr"   r"   r"   r#   Ú<module>   sP   ü(
,
