
    Kj[                        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
  S SKJrJr  S SKJr  S SKJr  S SKJrJr  \" \5      R-                  5       R.                  r\S-  r\S-  r\S	-  r\R8                  R;                  S
5      =(       d    SR=                  5       r\ " \5      r!\=(       d    Sr"\!(       a  \S-  S-  \"-  O\S-  r#\#S-  r$\#RK                  SSS9  \$RK                  SSS9  \#S-  r&\RN                  " 5       r(S r)S r*S r+S(S jr,S)S jr-S)S jr.S r/S*S jr0S r1S r2S+S jr3 " S S5      r4 " S  S!5      r5\5" 5       r6S"r7 " S# S$\5      r8S% r9S& r:\;S':X  a  \<" \:" 5       5      eg),    N)datetime)ThreadingHTTPServerBaseHTTPRequestHandler)Path)urlparse)Requesturlopenzplatform_config.jsontenantslocation_profile.jsonNWALS_TENANT legacyruntime	instanceslogsTparentsexist_okzplatform_launcher.pidc                  J    [         R                  " 5       R                  S5      $ )Nz%Y-%m-%d %H:%M:%S)r   nowstrftime     '/mnt/data/v089work/platform_launcher.pynow_textr   !   s    <<>""#677r   c                 z    [        U SSS9 n[        R                  " U5      sS S S 5        $ ! , (       d  f       g = f)Nrz	utf-8-sigencoding)openjsonload)pathfs     r   	load_jsonr%   %   s'    	dC+	.!yy| 
/	.	.s   ,
:c                     U R                  U R                  S-   5      n[        USSS9 n[        R                  " XSS9  S S S 5        UR                  U 5        g ! , (       d  f       N = f)Nz.tmpwutf-8r      indent)with_suffixsuffixr    r!   dumpreplace)r#   datatmpr$   s       r   	save_jsonr2   *   sQ    


4;;/
0C	c3	)Q		$!$ 
*KK 
*	)s   A
A(c                      [         R                  " U[        U 5      4US9    S S S 5        g! , (       d  f       g = f! [         a     gf = f)NtimeoutTF)socketcreate_connectionintOSError)porthostr5   s      r   	port_openr<   1   sA    %%tSY&7I JII s#    > -> 
;> > 
A
Ac                      [        XS9 nSUR                  s=:*  =(       a    S:  Os  sS S S 5        $ ! , (       d  f       g = f! [         a     gf = f)Nr4        F)r	   status	Exception)urlr5   r   s      r   http_okrC   9   sB    S*a!((((S( +** s*   	A 4	A 
AA A 
AAc                      [        U SSS.S9n[        X!S9 n[        R                  " UR	                  5       R                  S5      5      sS S S 5        $ ! , (       d  f       g = f! [         a     g f = f)Nzno-cachezNWALPlatformBuildCheck/1.0)Cache-Control
User-Agentheadersr4   r(   )r   r	   r!   loadsreaddecoderA   rB   r5   reqr   s       r   	http_jsonrN   A   s[    cZOk#lmS*a::affhoog67 +** s.   A& 3A	A& 
A#A& #A& &
A32A3c                     [         R                  S:w  a  gS[        U 5       S3n [        R                  " SSSU/[        R
                  [        R
                  S9R                  S	:H  $ ! [         a     gf = f)
zMTerminate the listener only after it has identified as a stale NWAL renderer.ntFz&$pids=Get-NetTCPConnection -LocalPort z -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique; if(-not $pids){exit 1}; foreach($ownerPid in $pids){taskkill /PID $ownerPid /T /F | Out-Null}; exit 0
powershell
-NoProfile-Commandstdoutstderrr   )osnamer8   
subprocessrunDEVNULL
returncoderA   )r:   scripts     r   terminate_windows_port_ownerr^   J   s    	ww$
0T <X 	X ~~|\:vN%/%7%7
@R@RTT^T^bcd 	d s   AA) )
A65A6c                     U (       d  g [        U SS0S9n[        X!S9 nSUR                  s=:*  =(       a    S:  Os  sS S S 5        $ ! , (       d  f       g = f! [         a     gf = f)NFrF   zLocalOperationsPlatform/0.0.3rG   r4   r>   r?   )r   r	   r@   rA   rL   s       r   remote_http_okr`   [   sZ    cL2Q#RSS*a!((((S( +** s-   A A	A 
AA A 
A&%A&c                  "    [         R                  $ N)sys
executabler   r   r   find_pythonre   f   s    >>r   c                  h    [         R                  " S5      =(       d    [         R                  " S5      $ )Nznode.exenode)shutilwhichr   r   r   	find_noderj   j   s    <<
#;v||F';;r   c                 (   [         R                  S:w  a  gU R                  SS5      nUS:X  a  SOSnSS	S
SU-   S-   U-   S-   /n [        R                  " U[        R
                  [        R
                  S9R                  S:H  $ ! [         a     gf = f)NrP   F'z''rg   z^node\.exe$z(^py(?:thon(?:w)?(?:3(?:\.\d+)?)?)?\.exe$rQ   rR   rS   zB$p=Get-CimInstance Win32_Process | Where-Object { $_.Name -match 'z' -and $_.CommandLine -like '*z *' }; if($p){exit 0}else{exit 1}rT   r   )rW   rX   r/   rY   rZ   r[   r\   rA   )textkindescapedname_patterncmds        r   process_exists_with_textrr   n   s    	ww$ll3%G%)V^>9dLlJ	)	*-"	"$+	,/(	(	C~~c**<*<ZEWEWXccghhh s   >B 
BBc                   J    \ rS rSrS rS rS rS rS rS r	S r
S	 rS
 rSrg)ManagedService   c                     Xl         US   U l        US   U l        S U l        SU l        SU l        SU l        S U l        SU l        S U l	        [        U R                   S3-  U l        S U l        g )NkeyrX   stoppedStoppedr   r   z.log)specrw   rX   processmoder@   
last_error
started_atrestart_countunhealthy_sinceLOG_DIRlog_path
log_handle)selfrz   s     r   __init__ManagedService.__init__   sn    	;L		#TXXJd"33r   c                 V   U R                   R                  S5      nU R                   R                  S5      nU(       a+  [        XR                   R                  SS5      5      (       d  gU R                  5       (       d  gU(       a  [	        U5      (       d  g[        U=(       d    U5      $ )Ndetect_textr:   rn   pythonF)rz   getrr   output_freshr<   bool)r   detectr:   s      r   detect_externalManagedService.detect_external   sx    }-yy}}V$2699==QY;Z[[   ""	$FNd##r   c                    U R                   R                  S5      nU(       a  U R                   R                  S5      nU(       ag  [        U5      n [        UR                  U5      =(       d    S5      n[
        R
                  " 5       U-
  U R                   R                  SS5      :  a  gO[        U5      (       d  gU R                   R                  S5      nU(       a  [        U5      nUR                  5       (       aN  [
        R
                  " 5       UR                  5       R                  -
  U R                   R                  SS	5      :  a  gU R                   R                  S
5      nU(       ad   [        [        U5      R                  U5      =(       d    S5      n[
        R
                  " 5       U-
  U R                   R                  SS	5      :  a  gg! [         a     gf = f! [         a     gf = f)N
health_urlhealth_json_epoch_fieldr   Fhealth_max_ageZ   
watch_filewatch_max_age   watch_json_epoch_fieldT)rz   r   rN   floatrA   timerC   r   existsstatst_mtimer%   )r   r   epoch_fieldpayloadvaluer   ps          r   r   ManagedService.output_fresh   sv   YY]]<0
))--(ABK#J/!!'++k":"?a@E 99;&$))--8H"*MM  NZ((YY]]<0
Z A88::qvvx/@/@!@DIIMMRacfDg!g))--(@AK!!)A,"2"2;"?"D1EE 99;&$))--*MM ' ! ! ! ! ! !s$   #G 1,G% 
G"!G"%
G21G2c                    U R                   R                  S5      S:w  a  g[        U R                   S   5      nUS-  R                  5       (       a  g[        R
                  " S5      =(       d    [        R
                  " S5      nU(       d  SU l        g	S
U l        [        R                  " US/[        U5      SSS9n[        U R                  SSSS9 nUR                  UR                  =(       d    S5        UR                  UR                  =(       d    S5        S S S 5        UR                   S:w  a  SU l        g	g! , (       d  f       N'= f)Nrn   rg   Tcwdnode_modulesznpm.cmdnpmzNode.js/npm not foundFzInstalling Node dependenciesinstall)r   capture_outputrm   ar(   r/   )r   errorsr   r   z#npm install failed; see service log)rz   r   r   r   rh   ri   r}   r@   rY   rZ   strr    r   writerU   rV   r\   )r   r   r   resultr$   s        r   ensure_dependencies"ManagedService.ensure_dependencies   s    99== F*499U#$. ((**ll9%<e)<5DO4i 0c#htZ^_$--wyIQGGFMM'R(GGFMM'R( J !CDO JIs   A	D==
Ec                    U R                   R                  SS5      (       d  SU l        SU l        g U R                   R                  S5      nU R                   R                  S5      nU R                   R                  S5      nU(       Ga/  U(       Ga'  [	        U5      (       Ga  U(       a  [        U5      OS n[        U[        5      (       a  UR                  S5      OS nXQ:X  a  S	U l        S
U S3U l        g U(       a  SU 3U l        [        U5      (       d   SU l        SU l        SU SU SU S3U l	        g [        S5       H*  n[	        USS9(       d    O[        R                  " S5        M,     [	        U5      (       a  SU l        SU l        SU SU S3U l	        g OSU l        SU l        SU S3U l	        g U R                  5       (       a  S	U l        SU l        g U(       a*  [	        U5      (       a  SU l        SU l        SU S3U l	        g U R                  5       (       d  SU l        SU l        g U R                   S   nU R                   S    n[        U R                   S!S"S#S$S%9U l        U R"                  R%                  S&['        5        S'U R(                   S(U S)35        S*n	[*        R(                  S+:X  a!  [,        R.                  [,        R0                  -  n	 [,        R2                  " U[5        U5      U R"                  [,        R6                  [,        R8                  U	0 [*        R:                  EU R                   R                  S,5      =(       d    0 R=                  5        V
Vs0 s H  u  p[5        U
5      [5        U5      _M     snn
ES-9U l        S.U l        S/U l        [        R                  " 5       U l         S U l!        g s  snn
f ! [D         a(  nSU l        S0U l        [5        U5      U l	         S nAg S nAff = f)1NenabledTdisabledzDisabled for this profile expected_incident_motion_versionr:   r   incident_motion_versionexternalzRunning verified z externallyzStopping stale renderer failedz#Stale renderer could not be stoppedzPort z	 reports z; z6 is required. Run STOP_PLATFORM.bat, then start again.   g?r4   z%Port still occupied by stale rendererz must be free before z can start.z&Unknown service occupies renderer portzK is in use but did not identify as an NWAL renderer. It was left untouched.)Running externally (live output verified)z#Port occupied; live output is stalez< is open, but this service did not pass its live-data check.zDependency setup failedcommandr   r   r(   r/      )r   r   	bufferingz
[z] START z: 
r   rP   env)r   rU   rV   stdincreationflagsr   managedStartingzFailed to start)#rz   r   r|   r@   r<   rN   
isinstancedictr^   r}   ranger   sleepr   r   r    r   r   r   r   rX   rW   rY   CREATE_NO_WINDOWCREATE_NEW_PROCESS_GROUPPopenr   STDOUTr[   environitemsr{   r~   r   rA   )r   expected_versionr:   r   identityactual_version_rq   r   r   kvexcs                r   startManagedService.start   sn   yy}}Y--"DI5DK99==)KLyy}}V$YY]]<0
40:y,HHRS[]aHbHbX\\*CDhlN1&	 12B1C;O 88HI3D99 (DI"GDK(-dV9^<LBO_N`  aW  'XDOrA$T37JJsO # T?? (DI"IDK(-dV3HIYHZZe&fDO	 # %	F$)$/z"{!!"DIEDKIdOO DI?DK %dV+ghDO'')) DI3DKii	"iit}}cGIabcHJ<x		{"SELM77d?&77*:]:]]M	'%++H!(( ((+grzzg$))--PUBVB\Z\AcAcAe%fAec!fc!fnAe%fgDL "DI$DK"iikDO#'D  &g  	' DI+DK!#hDOO	's+   BO	 &!O;O	 O	 	
O;O66O;c                     U R                   S:X  a  gU R                   S:X  a  U R                  5       $ U R                   S:w  d(  U R                  b  U R                  R                  5       b  gU R	                  5       $ )Nr   Tr   r   F)r|   r   r{   pollr   r   s    r   healthyManagedService.healthy  se    99
"99
"''))99	!T\\%9T\\=N=N=P=\  ""r   c                 \   U R                   S:X  a  g U R                   S:X  aQ  U R                  5       (       a  SU l        g SU l        SU l         U =R                  S-  sl        U R	                  5         g U R                   S:X  Ga  U R
                  b  U R
                  R                  5       bC  SU l        U =R                  S-  sl        [        R                  " S5        U R	                  5         g U R                  5       (       a  S	U l        S U l
        g U R                  (       a.  [        R                  " 5       U R                  -
  S
:  a  SU l        g U R                  c  [        R                  " 5       U l
        [        R                  " 5       U R                  -
  n[        U R                  R                  SS5      5      nX:  aS  SU l        U =R                  S-  sl        U R                  5         [        R                  " S5        U R	                  5         g S[!        U5       S3U l        g g )Nr   r   r   z-External output stopped or stale; taking overrx   r   r   zExited; restartingRunningr   r   restart_unhealthy_after<   zLive output stale; restartingz$Process running; live output stale (zs))r|   r   r@   r   r   r{   r   r   r   r   r   r~   r   rz   r   stopr8   )r   	stale_forrestart_afters      r   checkManagedService.check#  s   99
"99
"##%%I 	 N%	""a'"

99	!||#t||'8'8':'F2""a'"

1

||~~''+$ ??tyy{T__'Dr'I",DK++3/3yy{, $		d.B.B BI$)$))--8QSU*V$WM 1&E**a/*		

1

(LSQZ^L\\^&_5 "r   c                 $   U R                   S:w  d  U R                  (       d  g  [        R                  S:X  aU  [        R
                  " SS[        U R                  R                  5      SS/[        R                  [        R                  S9  OU R                  R                  5          S U l        SU l         S	U l        U R                  (       a#   U R                  R                  5         S U l        g g ! [         a     NVf = f! [         a     N%f = f)
Nr   rP   taskkillz/PIDz/Tz/FrT   rx   ry   )r|   r{   rW   rX   rY   rZ   r   pidr[   	terminaterA   r@   r   closer   s    r   r   ManagedService.stopK  s    99	!	ww$
FC8H8H4I4QUV_i_q_q  {E  {M  {M  N&&( 	??%%' #DO   		  s*   A(C2 C2 D 2
C?>C?
DDc                     U R                   U R                  U R                  U R                  U R	                  5       U R
                  U R                  [        U R                  R                  [        5      5      S.$ )N)rw   rX   r|   r@   r   r   r}   log)rw   rX   r|   r@   r   r   r}   r   r   relative_toBASE_DIRr   s    r   as_dictManagedService.as_dict_  sX    88IIIIkk||~!////t}}00:;	
 		
r   )rw   r}   r   r   r|   rX   r{   r   rz   r~   r@   r   N)__name__
__module____qualname____firstlineno__r   r   r   r   r   r   r   r   r   __static_attributes__r   r   r   rt   rt      s3    $:(I'V#&`P#(

r   rt   c                   V    \ rS rSrS rS rS rS rS rS r	S r
S	 rS
 rS rS rSrg)Platformil  c                 ^   [        [        5      U l        [        =(       d    U R                  R	                  SS5      U l        U R                  U R
                  5      U l        U R                  5         0 U l	        [        R                  " 5       U l        [        R                  " 5       U l        g )Nactive_tenantznw-al-scanner)r%   CONFIG_PATHconfigTENANT_OVERRIDEr   	tenant_idload_tenanttenantapply_tenant_location_profileservices	threadingRLocklockr   r~   r   s    r   r   Platform.__init__m  sm    ,(]DKKOOO_,]&&t~~6**,OO%	))+r   c                 L    [        S [        R                  S5       5       5      $ )Nc              3   L   #    U  H  oR                   R                  v   M     g 7frb   )parentrX   ).0r   s     r   	<genexpr>&Platform.tenant_ids.<locals>.<genexpr>w  s     P-Ohhmm-Os   "$z*/station.json)sortedTENANTS_DIRglobr   s    r   
tenant_idsPlatform.tenant_idsv  s    P[-=-=>N-OPPPr   c                 v    [         U-  S-  nUR                  5       (       d  [        SU 35      e[        U5      $ )Nzstation.jsonzTenant profile not found: )r  r   FileNotFoundErrorr%   )r   r   r#   s      r   r   Platform.load_tenanty  s9    Y&7{{}}#&@$LMMr   c                    U R                   R                  S5      nU(       d  g[        (       a  g[        U-  R	                  5       n UR                  [        R	                  5       5        UR                  5       (       d  [        SU 35      e[        U5      n[        R                  S5      nUR                  [        R                  " USS9S-   S	S
9  UR                  [        5        g! [         a    [        S5      ef = f)a  Materialize the selected tenant's location profile at the legacy root path.

Broadcast V3 and Incident Command intentionally continue reading
/location_profile.json so production HTML does not need a redesign.
Tenant selection now controls which profile is copied there.
location_profileNz<Tenant location_profile must stay inside the platform folderz#Tenant location profile not found: z	.json.tmpr)   r*   r   r(   r   )r   r   INSTANCE_LOCKEDr   resolver   
ValueErrorr   r  r%   LOCATION_PROFILE_PATHr,   
write_textr!   dumpsr/   )r   relsrcprofiler1   s        r   r   &Platform.apply_tenant_location_profile  s     kkoo01?#~&&(	]OOH,,./ zz||#&I#$OPPC.#//<tzz'!4t;gN)*  	][\\	]s   #C* *D c                    U R                   R                  S0 5      nU R                   R                  S5      =(       d    0 n[        UR                  SS5      5      n[        UR                  SS5      5      n[        UR                  SS5      5      n[        UR                  S	S
5      5      n[        5       n[	        5       nUR                  SS5      R                  5       n	U R                   R                  S5      =(       d    Sn
[        U
-  R                  5       n[        S-  nUR                  SSS9  US-  nUS-  n[        S-  nUR                  5       (       d  UR                  SSS9  U R                  [        [        5      [        U5      S.nSS[        UR                  SS5      5      S[        USSS/0 UEU[        U5      SU S3[        U5      [        U5      S.ESUSU S 3S!S".SS#[        UR                  SS$5      5      S[        USSS%/0 UES&U0ES%USU S3S'S(S)S*.SS+U	S,;   S[        USSS-S.[        U5      /US-US/S0S/S1.S2S3[        UR                  S2S5      5      S4[        S5-  S6-  U=(       d    S4S7/0 UEUSU S83SU S93[        [        S-  S:-  5      S;.ES6USU S<3S=.
S	S>[        UR                  S	S$5      5      S[        USSS?/0 UEU[        [        S@-  U R                  -  SA-  5      SB.ES?USU S<3S=.
/$ )CNr   portscore(#  peet  	lightningQ  radio_intelligencei=#  lightning_providersparkr  r   Tr   zspark_lightning.jsonzspark_lightning_status.jsonz
calls.jsonz[]
r(   r   )r   NWALS_RUNTIME_DIRNWALS_LOCATION_PROFILEzCore API + Incident Rendererr   z-Xutf8zserve_v097_incident_video.pyzhttp://127.0.0.1:z/api/data/currentdata)
NWALS_PORTNWALS_CALLS_FILENWALS_PEET_URLNWALS_SPARK_DATA_FILENWALS_SPARK_STATUS_FILEz/api/render/statusv030)rw   rX   r   rn   r   r   r   r   r:   r   r   zPEET / HazCam Weather ParserFzpeet_server_hazcams_wu.pyNWALS_PEET_PORTLastDataReadEpochr   x   )rw   rX   r   rn   r   r   r   r   r:   r   r   r   r   zSpark Lightning Bridge>   r(  hazcamszspark_lightning_bridge_v003.pyz--output-dirr   last_successful_write_epoch)rw   rX   r   rn   r   r   r   r   r   r   r   r   lightning_dashboardzLightning / Radar Dashboardrg   moduleszlightning-radar-dashboardz	server.jsz/api/lightningz/api/lightning/statuszstrike-history.json)PORTSPARK_API_URLSPARK_STATUS_URLHISTORY_FILE/health)
rw   rX   r   rn   r   r   r   r   r:   r   u   Radio Intelligence · Phase 3zradio_intelligence_server.pyr
   zradio_intelligence.json)NWALS_RADIO_PORTNWALS_RADIO_CONFIG)r   r   r8   re   rj   lowerr   r  RUNTIME_DIRmkdirr   r  r   r   r   )r   service_cfgr  	core_port	peet_portlightning_port
radio_portpyrg   providerprofile_relprofile_pathlightning_dir
spark_dataspark_status
calls_file
common_envs                    r   build_specsPlatform.build_specs  s   kkooj"5(.B		&$/0			&$/0	UYY{D9:#7>?
]{??#7AGGIkkoo&89T=T ;.779#k1D48"%;;
$'DD </
  ""!!&7!; NN!$[!1&),&7

 'E => f.LMF* F&/,/
O,=i[H]*^14Z36|3DF  >y 1)<NO4:  'E >? f.IJC*C&7C:I 1)<QR+>RT+.
 (@#';; f.NP^`cdq`rs!?*R*G+-
 -6S0Et LMx)';>Y'Y NFK8`* ` .+<YK~)V.?	{J_,`(+K+,EH],](^	`
  ;N 1.1AI ,5T0De LM f.LMv* v,6.1(Y2F2WZs2s.tv  >z 1*WE
g>
 >	
r   c                     U R                      0 U l        U R                  5        H6  n[        U5      nX R                  UR                  '   UR                  5         M8     S S S 5        g ! , (       d  f       g = frb   )r  r   rQ  rt   rw   r   )r   rz   svcs      r   start_servicesPlatform.start_services  sP    YYDM((*$T*),cgg&		 + YYs   AA((
A6c                     U R                      U R                  R                  5        H  nUR                  5         M     S S S 5        g ! , (       d  f       g = frb   )r  r   valuesr   r   rT  s     r   stop_managedPlatform.stop_managed  s3    YY}}++-
 . YYs   2A
Ac                    [         (       a  [        S5      eU R                     XR                  5       ;  a  [        S5      eU R	                  5         Xl        U R                  U5      U l        U R                  5         XR                  S'   [        [        U R                  5        U R                  5         S S S 5        g ! , (       d  f       g = f)Nz\This launcher is locked to one tenant. Start the other tenant with its own START_*.bat file.zUnknown tenantr   )r  r  r  r  rZ  r   r   r   r   r   r2   r   rU  )r   r   s     r   switch_tenantPlatform.switch_tenant  s    ?{||YY 11 !122&N**95DK..0+4KK(k4;;/! YYs   BB??
Cc                    [         R                  [        U R                  R	                  SS5      5      5      (       d  U R
                     U R                  R                  5        H  nUR                  5         M     S S S 5        [         R                  [        U R                  R	                  SS5      5      5      (       d  M  g g ! , (       d  f       NO= f)Nservice_check_seconds   )	
STOP_EVENTwaitr   r   r   r  r   rX  r   rY  s     r   monitorPlatform.monitor
  s    //%8OQR(S"TUU==//1CIIK 2  //%8OQR(S"TUUs   
2C
Cc           
      d   U R                   R                  S5      =(       d    0 n[        UR                  S5      =(       d    S5      n[        U R                   R                  S5      =(       d    0 R                  SS5      5      n[        (       a  SU 3O%[        UR                  S5      =(       d    SU 35      R                  S5      n[        UR                  S	5      =(       d    S5      R                  S5      nUS
-   nU(       a  US
-   OSn[        USS9nUR                  SS5      (       a
  [        USS9OSn	[        UR                  S5      =(       d    S5      R                  5       n
U
S:X  a  U	(       a  SUpOU(       a  SUpOU	(       a  SUpOSUpU
UU	UX-   XB-   U(       a  XR-   OSUUS.	$ )Nr$  queryr   r  r%  http://localhost:local_base_url/remote_base_urlr=  g333333?r4   fallback_enabledT      ?F	preferredlocalremotezremote-fallbackoffline)	rn  local_healthyremote_healthyactive_source
active_url	local_url
remote_urlri  rk  )	r   r   r   r8   r  rstriprC   r`   r@  )r   cfgrg  rF  
local_baseremote_baselocal_health_urlremote_health_urlrr  rs  rn  rt  active_bases                r   lightning_statusPlatform.lightning_status  s   kkook*0bCGGG$*+dkkoog6<"AA+tTU>Mo).)9:SVWZW^W^_oWp  XY  wH  IW  HX  uY  TZ  b  b  cf  g
#''"34:;BB3G%	19D[94" 0#>KN77SegkKlKl(93Grw,78>>@	 ^)1;;)0*;):K;)2J;"*,*%-#+3>;.B(*

 
	
r   c                 Z   U R                      U R                  5       n[        U R                  5      n[        U R                  R	                  S5      =(       d    0 5      US'   U R                  R	                  S5      =(       d    0 n[        UR	                  SS5      5      nSU 3n[        UR	                  SS5      5      nUS   R                  US	-   US
-   US-   US-   US-   US-   US   SU S3SU S3SU S3SU S3S.5        SU R                  R	                  SS5      U R                  UU[        (       a  U R                  /O.U R                  5        Vs/ s H  opR                  U5      PM     sn[        [        [        R                  [        5      5      U R                   R#                  5        Vs/ s H  oR%                  5       PM     sn[        [&        R&                  " 5       U R(                  -
  5      [+        5       S.sS S S 5        $ s  snf s  snf ! , (       d  f       g = f)Nlinksresolved_linksr  r   r!  rh  r"  r#  z/broadcast-v3.htmlz/broadcast_studio.htmlz/program_output.htmlz/incident_command.htmlz/broadcast_automation.htmlz/calls_admin.htmlru  rj  z/wxpanel_history_manager.htmlz/wxpanel_climate_manager.htmlz/weather_source_manager.html)	broadcaststudioprogram_outputincident_commandbroadcast_automationcalls_adminr$  wxpanelhistory_entryclimate_entryweather_sourceTplatform_namezLocal Operations Platform)okr  r   r   r$  r
   instance_lockedruntime_dirr   uptime_seconds	timestamp)r  r  r   r   r   r8   updater   r   r  r  r   r   rA  r   r   r   rX  r   r   r~   r   )	r   r$  tenant_payloadr  rD  baserE  tidrT  s	            r   r@   Platform.status/  s   YY--/I!$++.N/3DKKOOG4L4RPR/SN+,KKOOG,2EEIIfd34I&yk2DEIIfd34I+,33!$88!99"&)?"?$(+C$C(,/K(K#&99&|4.yk;#4YK?\!]#4YK?\!]$5i[@\"]5  !%B]!^!^^(&-<_T[[MdhdsdsduBvdu]`CSCSTWCXduBv#2";#:#:8#DE6:mm6J6J6LM6Ls[[]6LM"%diikDOO&C"D%Z+ Y6 Cw N= Ys+   EHH4AH8H7H
H
H*)r   r  r   r~   r   r   N)r   r   r   r   r   r  r   r   rQ  rU  rZ  r]  rd  r  r@   r   r   r   r   r   r   l  s<    &Q+0V
p
" 
>"r   r   u  <!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Local Operations Platform v033</title>
<style>
:root{--bg:#06101a;--panel:#0d1b29;--panel2:#112337;--line:#20384c;--text:#edf6ff;--muted:#8da7bb;--accent:#1da1f2;--good:#39d98a;--warn:#ffbd4a;--bad:#ff5d6c}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 80% 0,#12324a 0,transparent 32%),linear-gradient(145deg,#050b12,#081521 55%,#04090e);color:var(--text);font-family:Inter,Segoe UI,Arial,sans-serif;min-height:100vh}.shell{max-width:1500px;margin:auto;padding:28px}.top{display:flex;gap:18px;align-items:center;justify-content:space-between;margin-bottom:26px}.brand h1{margin:0;font-size:30px}.brand p{margin:6px 0 0;color:var(--muted)}select,button{background:#102438;border:1px solid var(--line);color:var(--text);padding:11px 14px;border-radius:10px;font-weight:700}.section-title{font-size:13px;letter-spacing:.18em;color:#74c8fa;text-transform:uppercase;margin:25px 0 12px}.apps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.card{background:linear-gradient(160deg,rgba(17,35,55,.96),rgba(8,21,33,.96));border:1px solid var(--line);border-radius:16px;padding:18px;min-height:164px;box-shadow:0 18px 45px rgba(0,0,0,.24)}.card h3{font-size:20px;margin:9px 0}.card p{color:var(--muted);line-height:1.45;margin:0 0 18px}.launch{display:inline-block;text-decoration:none;color:white;background:var(--accent);font-weight:800;padding:10px 14px;border-radius:9px}.services{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.svc{background:rgba(13,27,41,.93);border:1px solid var(--line);border-radius:14px;padding:15px}.svchead{display:flex;gap:9px;align-items:center}.dot{width:10px;height:10px;border-radius:50%;background:var(--bad);box-shadow:0 0 12px currentColor}.dot.good{background:var(--good)}.dot.warn{background:var(--warn)}.status{font-weight:800;margin-top:11px}.mode{color:var(--muted);font-size:13px;margin-top:5px}.notice{margin-top:20px;border:1px solid rgba(255,189,74,.35);background:rgba(255,189,74,.08);color:#ffd990;padding:14px;border-radius:12px;display:none}.footer{color:var(--muted);font-size:12px;margin-top:24px}@media(max-width:1000px){.apps{grid-template-columns:repeat(2,1fr)}.services{grid-template-columns:repeat(2,1fr)}}@media(max-width:650px){.shell{padding:16px}.top{align-items:flex-start;flex-direction:column}.apps,.services{grid-template-columns:1fr}}
</style></head><body><div class="shell"><div class="top"><div class="brand"><h1 id="platformName">Local Operations Platform</h1><p id="stationName">Loading station profile...</p></div><div><select id="tenantSelect"></select></div></div><div class="section-title">Applications</div><div class="apps" id="apps"></div><div class="section-title">Services</div><div class="services" id="services"></div><div class="notice" id="notice"></div><div class="footer" id="footer"></div></div>
<script>
const appDefs=[['broadcast_automation','Broadcast Automation','Arm/manual-control SweepWX weather takeover and tune lightning/warning triggers.'],['radio_intelligence','Radio Intelligence','Monitor local SDRTrunk and VOX call recordings before transcription/automation is enabled.'],['broadcast','Automated Channel','Continuous WeatherScan-style broadcast output.'],['studio','Broadcast Studio','Manual live production and fullscreen graphics.'],['program_output','Program Output','Standalone 16:9 program window for OBS capture.'],['incident_command','Incident Command','Create calls, update TV, copy graphics, and render MP4 clips.'],['lightning','Lightning / Radar','Open the integrated lightning monitoring and radar workstation.'],['wxpanel','WxPanel','Open the full-screen local weather observation panel.'],['weather_source','Weather Source Manager','Choose PEET/Davis sources and manage temperature/humidity calibration.'],['history_entry','WxPanel History Setup','Enter prior monthly rainfall totals for station onboarding.'],['climate_entry','Climate Normals Setup','Enter NOAA daily/monthly normals for WxPanel departures and climate reporting.'],['calls_admin','Calls Admin','Stable call-entry and incident management interface.']];
function browserSafeUrl(url,key){if(!url)return url;try{const u=new URL(url,window.location.href);if((u.hostname==='localhost'||u.hostname==='127.0.0.1')&&window.location.hostname)u.hostname=window.location.hostname;return u.toString()}catch(e){return url}}
async function api(path,opts){const r=await fetch(path,opts);if(!r.ok)throw new Error(await r.text());return r.json()}
function esc(s){return String(s??'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]))}
async function refresh(){const d=await api('/api/status');document.documentElement.style.setProperty('--accent',d.tenant.branding?.accent||'#1da1f2');platformName.textContent=d.platform_name;stationName.textContent=d.tenant.display_name+' · '+d.tenant.description;tenantSelect.innerHTML=d.tenants.map(t=>`<option value="${esc(t.id)}" ${t.id===d.tenant_id?'selected':''}>${esc(t.display_name)}</option>`).join('');apps.innerHTML=appDefs.map(([key,title,desc])=>{const links=d.tenant.resolved_links||d.tenant.links||{};const url=browserSafeUrl(links[key],key);const lightningNote=key==='lightning'?`<div class="mode" style="margin:0 0 12px">Source: ${esc((d.lightning?.active_source||'offline').replace('-', ' ').toUpperCase())}${d.lightning?.local_healthy?' · Local ready':''}${d.lightning?.remote_healthy?' · Remote ready':''}</div>`:'';return `<article class="card"><div style="font-size:26px">${({broadcast:'📺',studio:'🎬',program_output:'🖥️',incident_command:'🚨',broadcast_automation:'🌩️',lightning:'⚡',wxpanel:'🌡️',weather_source:'⚙️',history_entry:'📚',climate_entry:'🌤️',radio_intelligence:'📻',calls_admin:'📟'})[key]}</div><h3>${title}</h3><p>${desc}</p>${lightningNote}${url?`<a class="launch" href="${esc(url)}" target="_blank">Launch</a>`:'<span class="mode">Not configured</span>'}</article>`}).join('');const physical=d.services.map(s=>`<div class="svc"><div class="svchead"><span class="dot ${s.healthy?'good':s.mode==='disabled'?'warn':''}"></span><b>${esc(s.name)}</b></div><div class="status">${esc(s.status)}</div><div class="mode">${esc(s.mode.toUpperCase())}${s.restart_count?' · '+s.restart_count+' restarts':''}</div></div>`).join('');const lightningHealth=`<div class="svc"><div class="svchead"><span class="dot ${d.lightning?.local_healthy?'good':'warn'}"></span><b>Local Lightning Endpoint</b></div><div class="status">${d.lightning?.local_healthy?'Available':'Starting / Offline'}</div><div class="mode">${esc(d.lightning?.local_base_url||'')}</div></div><div class="svc"><div class="svchead"><span class="dot ${d.lightning?.remote_healthy?'good':'warn'}"></span><b>Remote Lightning Fallback</b></div><div class="status">${d.lightning?.remote_healthy?'Available':'Unavailable / Not configured'}</div><div class="mode">${esc(d.lightning?.remote_base_url||'None')}</div></div>`;services.innerHTML=physical+lightningHealth;if(d.tenant.limitations){notice.style.display='block';notice.textContent=d.tenant.limitations}else notice.style.display='none';footer.textContent=`Updated ${d.timestamp} · Platform uptime ${Math.floor(d.uptime_seconds/60)} min · PEET and Spark are verified from live output timestamps; stale files never count as running.`}
tenantSelect.addEventListener('change',async()=>{tenantSelect.disabled=true;try{await api('/api/tenant',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tenant_id:tenantSelect.value})});setTimeout(refresh,1000)}catch(e){alert(e.message)}finally{tenantSelect.disabled=false}});refresh();setInterval(refresh,3000);
</script></body></html>c                   ,    \ rS rSrS rS rS rS rSrg)DashboardHandlerif  c                    [        U R                  5      R                  nUS;   a  [        R                  S5      nU R	                  S5        U R                  SS5        U R                  S[        [        U5      5      5        U R                  5          U R                  R                  U5        g US:X  a%  U R                  S[         R#                  5       5        g U R                  SS	S
05        g ! [        [        [        [        4 a     g f = f)N>   r   rj  r(   r>   Content-Typeztext/html; charset=utf-8Content-Lengthz/api/status  error	not found)r   r#   DASHBOARD_HTMLencodesend_responsesend_headerr   lenend_headerswfiler   BrokenPipeErrorConnectionResetErrorConnectionAbortedErrorr9   json_responsePLATFORMr@   )r   r#   bodys      r   do_GETDashboardHandler.do_GETg  s    		"''9!((1Ds#^-GH-s3t9~>

  & = sHOO$563+ 67 $%9;QSZ[ s   C/ /DDc                    [        U R                  5      R                  n[        U R                  R	                  SS5      5      n [
        R                  " U R                  R                  U5      =(       d    S5      nUS:X  aS   [        R                  [        UR	                  SS	5      5      5        U R                  S
S[        R                  S.5        g U R                  SSS05        g ! [         a    U R                  SSS05         g f = f! [         a'  nU R                  SS[        U5      05         S nAg S nAff = f)Nr  0s   {}i  r  zinvalid jsonz/api/tenantr   r   r>   T)r  r   r  r  )r   r#   r8   rH   r   r!   rI   rfilerJ   rA   r  r  r]  r   r   )r   r#   lengthr0   r   s        r   do_POSTDashboardHandler.do_POSTy  s   		"''T\\%%&6<=	::djjoof5>?D = =&&s488K+D'EF""3t(BTBT(UV 3+ 67  	sWn$=>	  =""3#c((;<<=s+   8C, AD ,DD
ED<<Ec                    [         R                  " U5      R                  S5      nU R                  U5        U R	                  SS5        U R	                  SS5        U R	                  S[        [        U5      5      5        U R                  5          U R                  R                  U5        g ! [        [        [        [        4 a     g f = f)Nr(   r  zapplication/jsonrE   zno-storer  )r!   r  r  r  r  r   r  r  r  r   r  r  r  r9   )r   codeobjr  s       r   r  DashboardHandler.json_response  s    zz#%%g.4 );<*5)3s4y>:	JJT"!57MwW 	 		s   B, ,C	C	c                     g rb   r   )r   _argss     r   log_messageDashboardHandler.log_message  s    r   r   N)	r   r   r   r   r  r  r  r  r   r   r   r   r  r  f  s    8$8"r   r  c                      [         R                  5       (       a  g [         R                  5         [        R	                  5          [
        R                  SS9  g ! [         a     g f = f)NT)
missing_ok)rb  is_setsetr  rZ  PID_FILEunlinkrA   )r  s    r   cleanupr    sO    NN4( s   A 
A%$A%c            
        ^ [         R                  5       (       a   [        [         R                  5       R	                  5       5      n [
        R                  S:X  a  [        R                  " SSSU  3/SSS9n[        U 5      UR                  ;   as  [        SU  S35        [        S	[        R                  R                  S
5      =(       d    0 R                  S[        R                  R                  SS5      5       S35        g[         R#                  [        [
        R$                  " 5       5      SS9  [&        R&                  " [&        R(                  [*        5        [&        R&                  " [&        R,                  [*        5        [        R/                  5         [0        R2                  " [        R4                  SS9R7                  5         [        [8        (       aV  [        R                  R                  S
5      =(       d    0 R                  S[        R                  R                  SS5      5      O[        R                  R                  SS5      5      m[;        ST4[<        5      n[        S5        [        S5        [        S5        [        S[        R                  R                  S5       35        [        ST S35        [        S5        [        S5        [        S5        [        R                  R                  SS5      (       a)  [0        R>                  " SU4S j5      R7                  5          URA                  5         URC                  5         [+        5         g! [          a     GNff = f! URC                  5         [+        5         f = f)NrP   tasklistz/FIzPID eq T)r   rm   z,Platform launcher is already running as PID .zOpen http://localhost:r  
operationsdashboard_porti3#  rj  r   r(   r   )targetdaemonz0.0.0.0zD====================================================================zLOCAL OPERATIONS PLATFORM v029zProfile   : display_namezDashboard : http://localhost:zKPEET and Spark are started here unless a verified live copy already exists.zTUse the matching STOP_*.bat file to stop this instance and all services it launched.open_dashboard_on_startrm  c                  8   > [         R                  " ST  S35      $ )Nrh  rj  )
webbrowserr    )r:   s   r   <lambda>main.<locals>.<lambda>  s    Z__7Ha5P%Qr   r   )"r  r   r8   	read_textstriprW   rX   rY   rZ   r   rU   printr  r   r   r   rA   r  getpidsignalSIGINTr  SIGTERMrU  r   Threadrd  r   r  r   r  Timerserve_foreverserver_close)old_pidr   serverr:   s      @r   mainr    s   		(,,.4467Gww$"
EWWI;N'O`dkopw<5<</H	QRST2X__5H5H5Q5WUW4\4\]ikskzkzk~k~  @P  QU  lV  5W  3Y  YZ  [  \ BIIK(7;
MM&--)
MM&..'*H,,T:@@B  wF  wF##G,277hooFYFYZjlpFqr  LT  L[  L[  L_  L_  `p  rv  Lw  xD )T!24DEF	(O	
*+	(O	L,,^<=
>?	)$q
12	
WX	
`a	(O4d;;QRXXZ	1  		, 		s   C&M 1M- 
M*)M*-N	__main__)z	127.0.0.1g?)r)   )rm  )r   )=r!   rW   rh   r  r6   rY   rc   r   r   r  r   http.serverr   r   pathlibr   urllib.parser   urllib.requestr   r	   __file__r  r  r   r   r  r  r   r   r  r   r   r  INSTANCE_IDrA  r   rB  r  Eventrb  r   r%   r2   r<   rC   rN   r^   r`   re   rj   rr   rt   r   r  r  r  r  r  r   
SystemExitr   r   r   <module>r     s    	     
     C  ! +>!!#**//" #:: ::>>.17R>>@')BQh"[0;>W_bkWk

   $  . dT *00__
8
"<(g
 g
Te eP :4- 4n"J z
TV
 r   