
    1$jh                       S SK r S SKrS SKrS SKrS SKrS SKrS SKrS SKrS SKJrJ	r	  S SK
Jr  S SKJrJr  S SKJrJr  S SKJrJr  S SKJrJr  S SKJr  \" \5      R7                  5       R8                  r\S	-  S
-  r\S-  r1 Skr Sr!Sq"\RF                  " 5       r$S r%S r&S r'S r(SVS jr)S r*SWS jr+S r,S r-S r.S r/S r0S r1S r2SXS jr3S r4S r5S  r6S! r7S" r8SVS# jr9S$ r:S% r;S& r<S' r=SYS) jr>S* r?S+ r@S, rAS- rBS. rCS/ rDS0 rES1 rFSZS2 jrGS3 rHS4 rIS[S5 jrJS6 rDS7 rKS8 rLS9 rMS: rNS; rOS< rPS= rQS> rRS? rSS\S@ jrTSA rUS[SB jrV\!4SC jrWSD rXS]SE jrYSF rZSG r[SH r\ " SI SJ\5      r]S^SK jr^S^SL jr_S^SM jrTS[SN jrV\`SO:X  ag  \a" \R                  5      SP:  a  \c" \R                  SP   5      OSQrd\'" 5         \)" S(SR9  \\" 5         \e" SS\d ST35        \" SU\d4\]5      R                  5         gg)_    N)datetimetimezone)Path)ThreadingHTTPServerSimpleHTTPRequestHandler)urlparseparse_qs)Requesturlopen)	HTTPErrorURLError)load_databaseDATABASEzGRAFO_D_DATABASE.sqlitezrender_excel_like.py>   folders
event_metafolder_metagraph_set_cards   Fc                  V    [         R                  " S[         R                  " 5       5      $ )N%Y-%m-%dT%H:%M:%SZ)timestrftimegmtime     grafo_d_server.pynow_isor      s    ==-t{{}==r   c                      [         R                  " [        SS9n [         R                  U l        U R                  S5        U R                  S5        U R                  S5        U $ )N   timeoutzPRAGMA foreign_keys = ONzPRAGMA journal_mode = WALzPRAGMA busy_timeout = 5000)sqlite3connectDBRowrow_factoryexecutecons    r   
connect_dbr*   !   sI    
//"b
)CkkCOKK*+KK+,KK,-Jr   c                      [        5        n U R                  S5        U R                  S5        [        U 5        U R                  5         SSS5        g! , (       d  f       g= f)zFIdempotent schema extension inside the same canonical SQLite database.z
            CREATE TABLE IF NOT EXISTS ui_state (
                key TEXT PRIMARY KEY,
                value_json TEXT NOT NULL,
                created_at TEXT NOT NULL,
                updated_at TEXT NOT NULL
            )
        aL  
            CREATE TABLE IF NOT EXISTS api_ingest_log (
                id INTEGER PRIMARY KEY,
                api_id TEXT NOT NULL,
                event_legacy_id TEXT NOT NULL,
                action TEXT NOT NULL,
                created_at TEXT NOT NULL,
                UNIQUE(api_id, event_legacy_id)
            )
        N)r*   r'   repair_malformed_api_eventscommitr(   s    r   ensure_runtime_schemar.   *   sJ    	  	 	 	 		 	$C(

) 
s   >A
A c                  *    [        5         [        5       $ N)r.   r   r   r   r   load_dbr1   C   s    ?r   c                 n    S nU (       a  U" 5         g [         R                  " USS9R                  5         g )Nc                      [         R                  5       (       d  g [           [        R                  " [
        R                  [        [         5      /[        [        5      SS9  S S S 5        g ! , (       d  f       g = f)NF)cwdcheck)	RENDERexists_render_lock
subprocessrunsys
executablestrROOTr   r   r   _runrender_frontend.<locals>._runI   s>    }}NNCNNCK8c$iuU \\s   AA++
A9Ttargetdaemon)	threadingThreadstart)blockr?   s     r   render_frontendrH   H   s+    V
 T288:r   c                    U c  g[        U 5      R                  5       nU(       d  g [        U5      nUS:  a  US-  nUS:  d  US:  a  g[        R
                  " U[        R                  S9nUR                  S5      UR                  S5      UR                  UR                  UR                  4$ ! [         a     gf = f)	z^Return (iso_datetime, yyyy-mm-dd, year, month, day) for valid Unix epoch seconds/milliseconds.Nl    d(	 g     @@iCm8l    Wi )tzr   z%Y-%m-%d)r=   stripfloat	Exceptionr   fromtimestampr   utcr   yearmonthday)valuerawnumberdts       r   epoch_to_utc_partsrW   T   s    }
e*


Cs &	Vj0			8<<	8B
()
J


   s   B0 0
B=<B=c                     U HB  n[        U [        5      (       d  M  U R                  U5      S;  d  M1  U R                  U5      s  $    U$ )N)N )
isinstancedictget)mappingkeysdefaultkeys       r   first_presentra   n   s?    gt$$S)9)K;;s##  Nr   c                    [        U [        5      (       d  gU /nS H<  nU R                  U5      n[        U[        5      (       d  M+  UR                  U5        M>     SnSnU H;  n[	        XdS5      n[	        XeS5      nUc  M   Uc  M%  [        U5      [        U5      4s  $    U R                  S5      n	[        U	[        [        45      (       a+  [        U	5      S:  a  [        U	S   5      [        U	S	   5      4$ g)
zIExtract latitude/longitude from common API-center and ISS payload shapes.NN)iss_positionpositionlocationcoordinatesgeo)latitudelat)	longitudelonlnglongNrg         r   )	rZ   r[   r\   appendra   r=   listtuplelen)
item
candidatesr`   vallat_keyslon_keyssrcrj   rl   coordss
             r   extract_lat_lonr|   u   s    dD!!JMhhsmc4  c" N
 #H2HC40C40?ss8SX%%	  XXm$F&4-((S[A-=6!9~s6!9~--r   c                 "   [        U SS5      nU(       a  [        U5      $ [        U [        5      (       a  U R	                  S5      OS n[        U[        5      (       a%  UR                  5       (       a  UR                  5       $ Ub  Ub  SU SU 3$ g)N)placelocation_namecitycountryregionrY   rf   zLow Earth Orbit / lat z, lon )ra   r=   rZ   r[   r\   rK   )ru   rj   rl   explicitlocs        r   build_placer      s}    T#Z\^_H8}",T4"8"8$((:
dC#s		yy{
3?'uF3%88r   c                     [        U R                  S5      =(       d    U R                  S5      =(       d    S5      R                  5       nSU;   =(       d    [        U[        5      =(       a    SU;   $ )NnameidrY   ISSrd   )r=   r\   upperrZ   r[   )api_rowru   api_names      r   is_iss_payloadr      sR    7;;v&A'++d*;ArBHHJHHSD$!7!RNd<RSr   c                 &    U R                  S5        g)z
Remove malformed generic ISS rows produced by the earlier API mapper.
They are identifiable by an epoch value parsed as a year plus empty place.
Valid historical ISS_POS_* rows are not touched.
af  
        DELETE FROM events
        WHERE COALESCE(place, '') = ''
          AND COALESCE(date_year, 0) > 9999
          AND (
              UPPER(COALESCE(event_name, '')) LIKE '%ISS-LIVE-LOCATION%'
              OR UPPER(COALESCE(api_text, '')) LIKE '%ISS-LIVE-LOCATION%'
              OR UPPER(COALESCE(who, '')) = 'ISS-LIVE-LOCATION'
          )
        N)r'   r(   s    r   r,   r,      s     KK		r   c           	         [        U R                  S5      =(       db    U R                  S5      =(       dJ    U R                  S5      =(       d2    U R                  S5      =(       d    U R                  S5      =(       d    S5      R                  5       nSS Kn[	        U5      nU(       a7  Uu  pEpgnU R                  S5      =(       d    UU S'   XPS'   X`S'   XpS	'   XS
'   XgU4$ UR                  SU5      n	U	(       aw  [        U	R                  S5      5      nU	R                  S5      b  [        U	R                  S5      5      OSnU	R                  S5      b  [        U	R                  S5      5      OSnOUR                  SU5       V
s/ s H  n
[        U
5      PM     nn
U(       dI  UR                  S[        U R                  SS5      5      5       V
s/ s H  n
[        U
5      PM     nn
US S n[        U5      S:  a  US   OS n[        U5      S:  a  US   OSn[        U5      S:  a  US   OSnUbD  X`S'   XpS	'   XS
'   U R                  S5      (       d"  US:  a  US SUS SUS 3OU SUS SUS 3U S'   XgU4$ s  sn
f s  sn
f )Ndate	date_text	timestampr   rP   rY   r   r   rQ   rR   z+^(-?\d{1,6})(?:-(\d{1,2}))?(?:-(\d{1,2}))?$rp   ro      z-?\d+r   04d-02d)
r=   r\   rK   rerW   matchintgroupfindallrt   )eventrT   r   epochiso_datetimer   rP   rQ   rR   mxnums	name_numss                r   parse_event_dater      so   
eii  D599[#9  DUYY{=S  DW\W`W`agWh  Dlqluluv|l}  D  BD  E  K  K  MCs#E491c!IIj1A\j!ffgeC
?EA1771:#$771:#9AGGAJq!"!7c!''!*oQ "

8S 9: 91A 9:)+Hc%))FTVBW>X)YZ)YAQ)YIZRa=Dd)q.tAwdt9>QqTad1gQfgeyy  CG!8tCj%Ac#Y?TXSYYZ[`adZeefgjknfoQpE&M ;Zs   I7I<c                    [        U =(       d    0 5      nUR                  SUR                  S5      =(       d6    UR                  S5      =(       d    [        [        R
                  " 5       5      5        UR                  SUR                  S5      =(       d2    UR                  S5      =(       d    UR                  S5      =(       d    S5        UR                  S	UR                  S
5      =(       d2    UR                  S5      =(       d    UR                  S5      =(       d    S5        [        U5        UR                  SUR                  S5      =(       d    S5        UR                  SUR                  S5      =(       d    US   5        [        US   5      US'   [        US   5      US'   [        5       US'   UR                  SUS   5        U$ )Nr   event_id	legacy_idr   titlelabel
event_nameUntitled eventrowgraphgraph_idrow_keyr_   	start_colrF   rp   end_colend
updated_at
created_at)	r[   
setdefaultr\   r=   uuiduuid4r   r   r   )payloadr   s     r   normalize_eventr      sZ   BE	T599Z0_EIIk4J_cRVR\R\R^N_`	VUYYw/t599W3EtS_I`tdtu	UEIIg.l%))J2Gl599U^K_lclmU	[%))G"4"9:	Y		% 0 FE+4FGU;/0E+5+,E)!)E,	\5#67Lr   c                 *   [        U=(       d    S5      R                  5       =(       d    Sn[        5       nU R                  SU45      R	                  5       nU(       a  [        US   5      $ U R                  SX#U45      n[        UR                  5      $ )NUncategorized%SELECT id FROM folders WHERE name = ?r   zNINSERT INTO folders(name, is_open, created_at, updated_at) VALUES (?, 1, ?, ?))r=   rK   r   r'   fetchoner   	lastrowid)r)   r   folder_nametsr   curs         r   get_or_create_folderr      s}    d-o.446I/K	B
++=~
N
W
W
YC
3t9~
++X	"C s}}r   c                 h   [        U=(       d    S5      R                  5       =(       d    Sn[        5       nU R                  SU45      R	                  5       nU(       a  [        US   5      $ [        X5      nU R                  SU[        U=(       d    U5      XuU45      n[        UR                  5      $ )Nr_   'SELECT id FROM graphs WHERE row_key = ?r   z
        INSERT INTO graphs(row_key, visible_name, folder_id, color, created_at, updated_at)
        VALUES (?, ?, ?, NULL, ?, ?)
        )r=   rK   r   r'   r   r   r   r   )	r)   r   visible_namer   r`   r   r   	folder_idr   s	            r   get_or_create_graphr      s    
g"
#
)
)
+
8yC	B
++?#
H
Q
Q
SC
3t9~$S6I
++	 
c,%#&	r:C s}}r   c                 r    U R                  SU45      R                  5       n[        US   =(       d    S5      $ )NzGSELECT COALESCE(MAX(sort_index), 0) AS n FROM events WHERE graph_id = ?nr   )r'   r   r   )r)   r   r   s      r   max_sort_indexr   	  s3    
++_bjal
m
v
v
xCs3x}1r   c                 0   [        UR                  S5      =(       d2    UR                  S5      =(       d    UR                  S5      =(       d    S5      n[        XX!R                  S5      =(       d    UR                  S5      =(       d    S5      n[        U5      u  pEnUR                  S5      =(       d    UR                  S	5      nU(       d4  Ub1  US
 S[	        U=(       d    S5      S S[	        U=(       d    S5      S 3n[	        UR                  S5      =(       d&    UR                  S5      =(       d    [        X5      S-   5      n[	        UR                  S5      =(       d    UR                  S5      =(       d    U5      n	[	        UR                  S5      =(       d    [        SX-
  S-   5      5      n
[	        UR                  S5      =(       d    U5      n[        5       n0 S[        UR                  S5      =(       d-    UR                  S5      =(       d    [        R                  " 5       5      _SU_SU_S[        UR                  S5      =(       d    UR                  S5      =(       d    S5      _S	[        U=(       d    S5      _SU_SU_SU_SU_SU	_SU
_S[        UR                  S5      =(       d    UR                  S5      =(       d    S5      _S [        UR                  S 5      =(       d2    UR                  S!5      =(       d    UR                  S"5      =(       d    S5      _S#[        UR                  S#5      =(       dJ    UR                  S$5      =(       d2    UR                  S%5      =(       d    UR                  S&5      =(       d    S5      _S'[        UR                  S'5      =(       d    UR                  S(5      =(       d    S5      _S)[        UR                  S*5      =(       d    UR                  S)5      =(       d    S5      _SU_S+U0E$ ),Nr   r   r   r_   r   folderr   r   r   r   r   rp   r   r   rF   r   r   duration
sort_indexr   r   r   r   r   r   rY   	date_year
date_monthdate_dayr~   PLACEwhoWHOactorbrief_descriptionbriefBRIEFdescriptionsource_linkurlapi_textapir   )
r=   r\   r   r   r   r   maxr   r   r   )r)   r   r   r   rP   rQ   rR   r   r   r   r   r   r   s                r   event_to_sql_valuesr     sv   %))E"]eii	&:]eii>P]T]^G"3))G:L:vPUPYPYZbPc:vgvwH'.D		&!;UYY{%;I)Cj#ejq/#!6aCH1c7JK	EIIk*eeii.@eNSVDadeDefI%))I&G%))E*:GiHG599Z(KC73F3J,KLHUYY|,9	:J	BS4REIIk,BRdjjlSH 	7 	c%))F+Zuyy/FZJZ[	
 	Sb) 	T 	e 	C 	Y 	7 	H 	UYYw'C599W+=CD 	s599U#Suyy'7S599W;MSQST 	SII)* yy!yy! yy' 
* 	s599]3Muyy7GM2N+, 	C		%(GEIIj,AGRH-. 	j/0 	b1 r   c                   ^^^	 SSK nSSKJm  [        U =(       d    S5      R	                  5       nU(       d  [        S5      eUR                  5       R	                  5       nUR                  SS5      nUR                  SSU5      n0 S	S
_SS
_SS
_SS
_SS_SS_SS_SS_SS_SS_SS_SS_SS_SS_SS_SS_SS_0 S S_S!S_S"S#_S$S#_S%S#_S&S'_S(S'_S)S'_S*S'_S+S,_S-S,_S.S,_S/S,_S0S1_S2S1_S3S1_S4S5_ES5S5S6S6S6S6S7.EnS8 mS9 m	SCUUU	4S: jjnUR                  S;U5      nU(       a  U" UR                  S
5      5      $ UR                  S<U5      nU(       a'  U" UR                  S
5      UR                  S5      5      $ UR                  S=U5      nU(       a7  U" UR                  S
5      UR                  S5      UR                  S5      5      $ UR                  S>U5      nU(       a7  U" UR                  S5      UR                  S5      UR                  S
5      5      $ UR                  S?U5      nU(       a>  UR                  S
5      U;   a)  U" UR                  S5      XFR                  S
5         5      $ UR                  S@U5      nU(       aN  UR                  S5      U;   a9  U" UR                  S5      XFR                  S5         UR                  S
5      5      $ UR                  SAU5      nU(       aN  UR                  S
5      U;   a9  U" UR                  S5      XFR                  S
5         UR                  S5      5      $ [        SBU-   5      e)Dz
Deterministic smart date parser for manual UI/API events.

Accepted:
- -600
- -600-01
- -600-01-01
- 1989
- 1989-01
- 1989-01-01
- 01/01/1989
- 1/1/1989
- 01-01-1989
- 1989/01/01
- June 1989
- 4 June 1989
- Junio 1989
- 4 Junio 1989
r   N)r   rY   zdate is required, z\s+janrp   januaryeneenerofebro   februaryfebreromarr   marchmarzoapr   aprilabrabrilmay   mayojun   junejuniojul   julyjulioaug   augustagoagostosep	   sept	september
septiembreoct
   octoberoctubrenov      )november	noviembredecdecemberdic	diciembrec                 B    [        U 5      n U S:  a  U S $ [        U 5      $ )Nr   r   )r   r=   ys    r   format_year%smart_parse_date.<locals>.format_yeare  s$    FU!C.A.r   c                 d    [        U 5      n U S-  S:H  =(       a    U S-  S:g  =(       d    U S-  S:H  $ )Nr   r   d     )r   r  s    r   is_leap_year&smart_parse_date.<locals>.is_leap_yeari  s3    FA
>SA!=SA>r   c                   > [        U 5      n U S:X  d  U S:  d  U S:  a  [        S5      eUc  [        U 5      U S S 4$ [        U5      nUS:  d  US:  a  [        S5      eUc  T" U 5       SUS	 3XS 4$ [        U5      nU S:  a
  T" XU5        O9S
T" U 5      (       a  SOSS
SS
SS
S
SS
SS
/nUS:  d  X#US-
     :  a  [        S5      eT" U 5       SUS	 SUS	 3XU4$ )Nr   ii?B zyear out of rangerp   r  zmonth out of ranger   r            r   zday out of range)r   
ValueErrorr=   )r  r   d
month_daysr   r  r  s       r   r5   smart_parse_date.<locals>.checkm  s   F6Q[AJ0119q61dD((Fq5AF1229!!n%Qqg.d::Fq5qMLOObRRRQSUWY[]_acdJ1uq1u-- !344a.!1S'1S'2A!;;r   z(-?\d{1,6})z(-?\d{1,6})[-/](\d{1,2})z%(-?\d{1,6})[-/](\d{1,2})[-/](\d{1,2})z%(\d{1,2})[-/](\d{1,2})[-/](-?\d{1,6})u"   ([a-záéíóúñ]+)\s+(-?\d{1,6})u.   (\d{1,2})\s+([a-záéíóúñ]+)\s+(-?\d{1,6})u.   ([a-záéíóúñ]+)\s+(\d{1,2})\s+(-?\d{1,6})zunsupported date format: rc   )r   r   r   r=   rK   r!  lowerreplacesub	fullmatchr   )
rS   r   rT   txtmonthsr5   r   r   r  r  s
          @@@r   smart_parse_dater+  7  s   ( 
ekr

 
 
"C+,,
))+


C
++c3
C
&&c
"CqQ %q*11qa!*A 	q 1 &q 	q	 1	 $Q	 )0	
 	q
 ! 	q ! %a 	q ! %a 	q A  %a *21 	q ! )! .:1 	r b #,R 	r  "r"2F/?< <2 	^S)AQWWQZ  
0#6AQWWQZ,,
=sCAQWWQZQWWQZ88
=sCAQWWQZQWWQZ88
:C@AQWWQZ6!QWWQZ
!344
FLAQWWQZ6!QWWQZ
!3QWWQZ@@
FLAQWWQZ6!QWWQZ
!3QWWQZ@@
036
77r   c                 F   [        5         [        U =(       d    0 5      n[        UR                  S5      =(       dJ    UR                  S5      =(       d2    UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       nU(       d  [        S5      e[        UR                  S5      =(       d2    UR                  S5      =(       d    UR                  S	5      =(       d    S5      R	                  5       nU(       d  [        S
5      e[        UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       n[        U5      u  pVpx[        UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       n	[        UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       n
[        UR                  S5      =(       dJ    UR                  S5      =(       d2    UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       n[        UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       n[        UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       n[        5       n[        5        nUR                  S5        UR                  SU45      R                  5       nU(       a  [        US   5      nO[        XUS5      nUR                  SU45      R                  5       n[        US   =(       d    S 5      nU S!U S!U S!U 3nUR                  S"UUUUUUUUS S S U	U
UUUUUU45      nUR                  5         S#S#S#5        S$WR                  UUUUUUS%.$ ! , (       d  f       N#= f)&zu
Insert one manually-created event into canonical SQLite using smart date parsing.
This is used by POST /api/events.
r   r   r   	graph_rowrY   zrow_key/row/graph is requiredr   r   r   zevent name is requiredr   r   r~   r   r   r   r   r   r   r   r   r   r   sourcez	manual-uiBEGIN IMMEDIATEz5SELECT id, visible_name FROM graphs WHERE row_key = ?r   r   zSSELECT COALESCE(MAX(sort_index), 0) + 1 AS next_sort FROM events WHERE graph_id = ?	next_sortrp   :a  
            INSERT INTO events(
                legacy_id, graph_id, row_key, event_name,
                date_text, date_year, date_month, date_day,
                start_col, end_col, duration,
                place, who, brief_description, source_link, api_text,
                sort_index, created_at, updated_at
            )
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            NT)okr   r   r   r   r   r   r   )r.   r[   r=   r\   rK   r!  r+  r   r*   r'   r   r   r   r-   r   )r   datar   r   raw_dater   r   r   r   r~   r   r   r   r   r   r)   r   r   sort_rowr   r   r   s                         r   insert_manual_event_sqlr6    s   
 2D 	88E?	88G	 88K 	  eg  899 	88F	88G	 	
 eg  122488F#Btxx'<BCIIKH1A(1K.I*!<TXXg%6<"=CCEE
dhhuo6%6B
7
=
=
?C$% 	88G	88G	 88M"	  eg  dhh}-F%FBGMMOK488J'L488H+=LMSSUH	B	%&CJ
 (* 	
 5;'H*3/RH;;aK
 (* 	 +.3!4
iqAi[*F	kk	 !' 
D 	

o 
t mm  	 	s 
s   %CP
P c                 4	   [        5         [        U =(       d    0 5      n[        UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       n[        UR                  S5      =(       d2    UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       n[        UR                  S5      =(       d2    UR                  S5      =(       d    UR                  S	5      =(       d    S5      R	                  5       n[        UR                  S
5      =(       d    UR                  S5      =(       d    S5      R	                  5       nSnSnSnSn	U(       a  [        U5      u  pgp[        UR                  S5      =(       d    S5      R	                  5       n
[        UR                  S5      =(       d    S5      R	                  5       n[        UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       n[        UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       n[        UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       n[        5       n[        5        nUR                  S5        SnU(       a!  UR                  SU45      R                  5       nUc7  U(       a0  U(       a)  U(       a"  UR                  SX4U45      R                  5       nUc/  U(       a(  U(       a!  UR                  SX445      R                  5       nUc  [        S5      e[        US   5      nUR                  SU45      R                  5       nU=(       d    US   nUb  UOUS   nUb  UOUS   nUb  UOUS   nUb  U	OUS   nUR                  SUUUUUU
UUUU=(       d    US   UU45        UR                  SU45      R                  5       nUR                  5         SSS5         [        S S!S"S#9  S$[        W5      S%.$ ! , (       d  f       N(= f! [         a     N+f = f)&aX  
Update canonical event fields from the right-side technical card.

Matching policy:
1. legacy_id/id if present.
2. row_key + event_name + date_text if legacy id is unavailable.
3. row_key + event_name fallback.

This updates SQLite canonical columns:
event_name, date_text/year/month/day, place, who,
brief_description, source_link, api_text.
r   r   rY   r   r   r   r   r   r   r   r   Nr~   r   r   r   r   r   r   r   r/  zTSELECT id, legacy_id, row_key, event_name, date_text FROM events WHERE legacy_id = ?z
                SELECT id, legacy_id, row_key, event_name, date_text
                FROM events
                WHERE row_key = ? AND event_name = ? AND date_text = ?
                ORDER BY id DESC
                LIMIT 1
                z
                SELECT id, legacy_id, row_key, event_name, date_text
                FROM events
                WHERE row_key = ? AND event_name = ?
                ORDER BY id DESC
                LIMIT 1
                z$event not found for canonical updatez
            SELECT event_name, date_text, date_year, date_month, date_day,
                   place, who, brief_description, source_link, api_text
            FROM events
            WHERE id = ?
            r   r   r   a  
            UPDATE events
            SET event_name = ?,
                date_text = ?,
                date_year = ?,
                date_month = ?,
                date_day = ?,
                place = ?,
                who = ?,
                brief_description = ?,
                source_link = ?,
                api_text = ?,
                updated_at = ?
            WHERE id = ?
            z
            SELECT id, legacy_id, row_key, event_name, date_text,
                   date_year, date_month, date_day,
                   place, who, brief_description, source_link, api_text
            FROM events
            WHERE id = ?
            zevent-card-saver   r2  dirtystatusTr2  r   )r.   r[   r=   r\   rK   r+  r   r*   r'   r   r!  r   r-   record_persistence_auditrM   )r   r3  r   r   r   r4  r   r   r   r   r~   r   r   r   r   r   r)   r   r   currentnew_event_namenew_date_textnew_date_yearnew_date_monthnew_date_daysaveds                             r   update_event_card_sqlrD    s    2DDHHTNAdhh{&;ArBHHJI$((9%S%SDHHW<MSQSTZZ\GTXXl+Ztxx/?Z488GCTZXZ[aacJ488F#Btxx'<BCIIKHIIJH5Eh5O2	j!'R(..0E
dhhuo#
$
*
*
,C$% 	88M"	 eg	 
 dhh}-F%FBGMMOK488J'@488E?@bAGGIH	B	%&++f hj 
 ;7zi++ i0	 hj  ;7z++ %	 hj  ;CDDs4y>++ K
 (* 	 $<w|'<%.%:	@T%.%:	@T'0'<',BW#,#8xgj>Q  !/GJ/!	
@  K	
 (* 	 	

G 
J !2!DI
 e U 
N  s   !E6Q9 R
 9
R

RRc                    [        5         [        U 5      n[        5        nUR                  S5        [	        X25      nUR                  SUS   45      R                  5       nU(       a"  U(       d  UR                  5         [        S5      eU(       a  UR                  SU5        SnO9UR                  S5      =(       d
    [        5       US'   UR                  SU5        S	nUR                  5         S S S 5        WUS
.$ ! , (       d  f       N= f)Nr/  z)SELECT id FROM events WHERE legacy_id = ?r   zduplicate event ida?  
                UPDATE events
                SET graph_id=:graph_id, row_key=:row_key, event_name=:event_name,
                    date_text=:date_text, date_year=:date_year, date_month=:date_month,
                    date_day=:date_day, start_col=:start_col, end_col=:end_col,
                    duration=:duration, place=:place, who=:who,
                    brief_description=:brief_description, source_link=:source_link,
                    api_text=:api_text, sort_index=:sort_index, updated_at=:updated_at
                WHERE legacy_id=:legacy_id
                updatedr   a  
                INSERT INTO events(
                    legacy_id, graph_id, row_key, event_name, date_text, date_year,
                    date_month, date_day, start_col, end_col, duration, place, who,
                    brief_description, source_link, api_text, sort_index, created_at, updated_at
                )
                VALUES(
                    :legacy_id, :graph_id, :row_key, :event_name, :date_text, :date_year,
                    :date_month, :date_day, :start_col, :end_col, :duration, :place, :who,
                    :brief_description, :source_link, :api_text, :sort_index, :created_at, :updated_at
                )
                inserted)actionr   )r.   r   r*   r'   r   r   rollbackr!  r\   r   r-   )r   allow_upsertnormr)   valuesexistingrH  s          r   insert_event_sqlrN    s    5!D	%&$S/;;7&:M9O

(* 	 LLLN122KK	  F#'88L#9#FWYF< KK   F

U 
V t,,W 
s   CC<<
D
c                    [        5         [        5        nUR                  S5        UR                  S[        U 5      [        U 5      45      R	                  5       nU(       d  UR                  5          S S S 5        g 0 SUS   =(       d    [        US   5      _SUS   _SUS   _S	US
   =(       d    S_SUS   _SUS   _SUS   _SUS   _SUS   _SUS   _SUS   _SUS   _SUS   _SUS   _SUS   _SUS   _nUR                  U=(       d    0 5        [        U[        U5      5      nUS   US'   UR                  SU5        UR                  5         S S S 5        U$ ! , (       d  f       W$ = f)Nr/  z@SELECT * FROM events WHERE legacy_id = ? OR CAST(id AS TEXT) = ?r   r   r   r   r   r   r   r   rY   rP   r   rQ   r   rR   r   r   r   r   r~   r   r   r   r   r   r   db_ida  
            UPDATE events
            SET graph_id=:graph_id, row_key=:row_key, event_name=:event_name,
                date_text=:date_text, date_year=:date_year, date_month=:date_month,
                date_day=:date_day, start_col=:start_col, end_col=:end_col,
                duration=:duration, place=:place, who=:who,
                brief_description=:brief_description, source_link=:source_link,
                api_text=:api_text, sort_index=:sort_index, updated_at=:updated_at
            WHERE id=:db_id
            )
r.   r*   r'   r=   r   rI  updater   r   r-   )r   patchr)   r   r=  rL  s         r   update_event_sqlrS    s   	%&kkN]CM*
 (* 	 LLN 

#k"4c#d)n
3y>
 C%
 C$*	

 C$
 S&
 3z?
 [)
 s9~
 J
 S\
 3u:
 323
 3}-
 J
  #l+!
$ 	u{#$S/'*BCd)w	 	
 	

Y 
Z N[ 
Z Ns   AE<CE
E.c                    [        5         [        5        nUR                  S5        UR                  S[        U 5      [        U 5      45      nUR	                  5         UR
                  S:  sS S S 5        $ ! , (       d  f       g = f)Nr/  z>DELETE FROM events WHERE legacy_id = ? OR CAST(id AS TEXT) = ?r   )r.   r*   r'   r=   r-   rowcount)r   r)   r   s      r   delete_event_sqlrV    s^    	%&kkL]CM*
 	

||a 
s   AA55
Bc           
        ^ U(       d  g U Vs1 s H  n[        U5      iM     nnU R                  S5      R                  5       nU GH  nUS   n [        R                  " US   5      nSnUS:X  a  [        U[        5      (       a  S H^  n	[        UR                  U	5      [        5      (       d  M)  Xy    Vs/ s H  n[        U5      U;  d  M  UPM     n
nXU	   :w  d  MX  XU	'   SnM`     [        UR                  S5      [        5      (       a<  [        US   R                  5       5       H  n[        U5      U;   d  M  US   U	 SnM     US	;   aV  [        U[        5      (       aA  [        UR                  5       5       H$  m[        U4S
 jU 5       5      (       d  M  UT	 SnM&     U(       d  GMj  U R                  S[        R                  " USSS9[        5       U45        GM     g s  snf ! [
         a     GM  f = fs  snf )Nz$SELECT key, value_json FROM ui_stater`   
value_jsonFr   )deletedRows
deleteRowsTgraphFolder)r   r   r   c              3      >#    U  H7  n[        T5      R                  US -   5      =(       d    [        T5      U:H  v   M9     g7f)r1  N)r=   
startswith).0r"  meta_keys     r   	<genexpr>6cleanup_ui_state_after_graph_delete.<locals>.<genexpr>E  s8     `X_STs8}//C8NCMQ<NNX_s   ?Az@UPDATE ui_state SET value_json = ?, updated_at = ? WHERE key = ?ensure_ascii	sort_keys)r=   r'   fetchalljsonloadsrM   rZ   r[   r\   rr   r^   anydumpsr   )r)   deleted_rowsr   deletedrowsr   r`   rS   changedknew_list	graph_keyr_  s               @r   #cleanup_ui_state_after_graph_deleterq  %  s   +,|!s1v|G,;;=>GGID%j	JJs<01E )
5$ 7 72eiilD11+08M8as1vW7L8HM8+#+a"& 3 %))M2D99!%eM&:&?&?&A!BI9~0!-0;"& "C
 BBzRWY]G^G^ .`X_```h"G /
 7KKREF	SVW=  -  		  Ns#   G'G,7G>G>,
G;:G;c           
      z   [        5         SSKJn  / nU =(       d    /  HL  nU" [        U=(       d    S5      5      R	                  5       nU(       d  M4  XB;  d  M;  UR                  U5        MN     / n/ n[        5       n[        5        nUR                  S5        U H  nUS:X  a  UR                  USS.5        M  UR                  SU45      R                  5       n	U	(       d  UR                  US	S.5        M]  UR                  S
U	S   45      R                  5       S   n
U
(       a  UR                  USU
S.5        M  UR                  SU	S   45        UR                  U5        M     UR                  S5      R                  5       n	U	(       a   [        R                  " U	S   5      nO0 n[        U[        5      (       Ga  [        U5      nUR!                  S/ 5       Vs/ s H  n[        U5      U;  d  M  UPM     snUS'   UR!                  S0 5      n[        U[        5      (       a-  [#        UR%                  5       5       H  nX;   d  M
  X	 M     XS'   UR!                  S0 5      n[        U[        5      (       a4  [#        UR'                  5       5       H  u  nnUU;   d  M  SUU'   M     XS'   UR                  S[        R(                  " USSS9Xw45        UR+                  5         SSS5         [-        SSSS9  SUUUS.$ ! [         a    0 n GNif = fs  snf ! , (       d  f       N9= f! [         a     N<f = f)z
Physically delete explicitly selected empty folders from SQLite.

Rules:
- Only requested folders are evaluated.
- Uncategorized is protected.
- Non-empty folders are rejected.
- ui_state is cleaned only for deleted folders.
r   unquoterY   r/  r   	protected)r   reasonz+SELECT id, name FROM folders WHERE name = ?z	not foundz/SELECT COUNT(*) FROM graphs WHERE folder_id = ?r   z	not empty)r   rv  graphsz DELETE FROM folders WHERE id = ?3SELECT value_json FROM ui_state WHERE key='folders'rX  r   openr[  a!  
                INSERT INTO ui_state(key, value_json, created_at, updated_at)
                VALUES ('folders', ?, ?, ?)
                ON CONFLICT(key) DO UPDATE SET
                    value_json=excluded.value_json,
                    updated_at=excluded.updated_at
                FTrb  Nzdelete-empty-folderr2  r8  )r2  	requestedrk  skipped)r.   urllib.parsert  r=   rK   rq   r   r*   r'   r   rf  rg  rM   rZ   r[   setr\   rr   r^   itemsri  r-   r<  )folder_namesrt  namesr   r   rk  r{  r   r)   r   graph_countstatedeleted_setf
open_stategraph_folderr   r   s                     r   delete_empty_folders_sqlr  Q  s    $ERs17|$**,4D%LL  
 GG	B	%&D&$+FG++= hj 
 $+FG++AT hjK
 "))  
 KK:SYLINN4 9 < kkA

(* 	 

3|#45 EeT""g,K !99Y3 3aq6, 3 E)
 62.J*d++joo/0A'&M 1 !+f 99]B7L,--'+L,>,>,@'AOGV,0?W- (B (4m$KK EFO	 	

] 
` !6aM
 	 [   a 
d  sd   DLL'8LL6L<ALAL$AL2L- LLLL
L*-
L:9L:Tc           	         [        5         [        U [        5      (       a  U /n U =(       d    /  Vs/ s H<  n[        U5      R                  5       (       d  M#  [        U5      R                  5       PM>     n nU (       d	  SSSSS/ S.$ SnSnSnSn/ n[	        5        nUR                  S5        U  GH:  n	UR                  SU	45      R                  5       n
U
(       d  M.  [        U
S   5      nUR                  SX45      R                  5       nU Vs/ s H  n[        US   5      PM     nnU H%  nUR                  SU45      nUUR                  -  nM'     U H%  nUR                  S	U45      nUUR                  -  nM'     UR                  S
X45      nUUR                  -  nUR                  SU45      nUUR                  -  nUR                  (       d  GM)  UR                  U	5        GM=     [        X5        UR                  5         S S S 5        U(       a$  [	        5        nUR                  S5        S S S 5        SUUUUUS.$ s  snf s  snf ! , (       d  f       NL= f! , (       d  f       N2= f)NTr   )r2  deleted_graphsdeleted_eventsdeleted_apisdeleted_api_logsrl  r/  z0SELECT id, row_key FROM graphs WHERE row_key = ?r   z>SELECT id FROM api_registry WHERE target_graph = ? OR name = ?+DELETE FROM api_ingest_log WHERE api_id = ?%DELETE FROM api_registry WHERE id = ?z4DELETE FROM events WHERE graph_id = ? OR row_key = ?zDELETE FROM graphs WHERE id = ?VACUUM)r.   rZ   r=   rK   r*   r'   r   r   re  rU  rq   rq  r-   )row_keysvacuumr   r  r  r  r  physically_deleted_rowsr)   r   r   r   api_rowsrapi_idsapi_idr   s                    r   delete_graphs_sqlr    s_   (C  :)1RK1CFLLNAHK !
 	
 NNL 	%&GKKB
 hj 
 5;'H{{P" hj 
 .66Xs1T7|XG6!kkAI !CLL0  " "kk;I , " ++F#C cll*N++1C cll*N|||'..w7[  ^ 	,CI

g 
j \SKK!  (($,' W LJ 7' 
l \s=   "IIA5II&B+I1I II
I
I,c                 l   [        U =(       d    S5      R                  5       nU(       d  Sn/ nSnUR                  5        HI  nUR                  5       (       a  UR	                  U5        SnM-  U(       a  M6  UR	                  S5        SnMK     SR                  U5      R                  S5      =(       d    S$ )NrY   z	NEW GRAPHF_T	NEW_GRAPH)r=   rK   r   isalnumrq   join)r   rT   outprev_uschs        r   slugify_graph_keyr     s    
djb/


!C
CGiik::<<JJrNG7

3  GGCLs#2{3r   c           
         [        5         [        U =(       d    0 5      n[        UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       nU(       d  [        S5      e[        5       n[        5        nUR                  S5        UR                  SX#U45        UR                  SX245        UR                  S5      R                  5       nU(       a   [        R                  " US	   5      nO0 n[        U[        5      (       d  0 nUR                  S
/ 5        UR                  S0 5        UR                  S0 5        UR                  S/ 5        UR                  S/ 5        SUS
   ;  a  US
   R                  SS5        X&S
   ;  a  US
   R!                  U5        SUS   U'   UR                  S[        R"                  " USSS9X345        UR%                  5         SSS5         ['        SSSS9  SUS.$ ! [         a    0 n GNf = f! , (       d  f       N2= f! [         a     N5f = f)z
Create a folder physically in SQLite.

Canonical policy:
- folders table is the source of truth.
- ui_state is synchronized for UI open/visibility state.
- no graph and no event is created.
r   r   rY   zfolder name is requiredr/  z~
            INSERT OR IGNORE INTO folders(name, is_open, created_at, updated_at)
            VALUES (?, 1, ?, ?)
            z
            UPDATE folders
            SET is_open = 1,
                updated_at = ?
            WHERE name = ?
            rx  rX  r   r[  ry  rY  rZ  r   r   T	  
            INSERT INTO ui_state(key, value_json, created_at, updated_at)
            VALUES ('folders', ?, ?, ?)
            ON CONFLICT(key) DO UPDATE SET
                value_json=excluded.value_json,
                updated_at=excluded.updated_at
            Frb  Nzcreate-folderr2  r8  )r2  r   )r.   r[   r=   r\   rK   r!  r   r*   r'   r   rf  rg  rM   rZ   r   insertrq   ri  r-   r<  )r   r3  r   r   r)   r   r  s          r   create_folder_sqlr  7  s!    2Dtxx;488H#5;<BBDD233	B	%& rN	
 	 J	
 kkA

(* 	 

3|#45 E%&&EB'+$+r*%	"22)##A7Y'')##D)"fd ZZETBBK		
 	

{ 
~ $G $''Q  9 
B  sD   AH!.HC.H!>H2 HH!HH!!
H/2
H?>H?c                  \   [        5         [        5       n [        5        nUR                  S5      R	                  5       nU(       d  SSS.sSSS5        $  [
        R                  " US   5      n[        U[        5      (       d  SSS
S	.sSSS5        $ UR                  S/ 5      n[        U[        5      (       d  SSSS	.sSSS5        $ SnUR                  S5        U H  n[        U=(       d    S5      R                  5       nU(       d  M.  UR                  SU45      R	                  5       nUR                  SX`U 45        UR                  SX45        Ub  M{  US-  nM     UR                  5         SSS5        SWS.$ ! [         a    SSSS	.s sSSS5        $ f = f! , (       d  f       N2= f)z
Convert folders present in ui_state into canonical rows in folders.

This makes Save meaningful for folders that were created by older UI code
before POST /api/folders existed.
rx  Tr   )r2  syncedNrX  Fzinvalid ui_state folders json)r2  r  errorzui_state folders is not objectr   zui_state folders list missingr/  rY   r   z
                INSERT OR IGNORE INTO folders(name, is_open, created_at, updated_at)
                VALUES (?, 1, ?, ?)
                z=UPDATE folders SET is_open = 1, updated_at = ? WHERE name = ?rp   )r.   r   r*   r'   r   rf  rg  rM   rZ   r[   r\   rr   r=   rK   r-   )r   r)   r   r  r   r  r   befores           r   sync_ui_folders_to_sqlr    s    	B	kkA

(* 	 !, 
	XJJs<01E %&&17WX 
  ))Ir*'4((17VW% 
( %&Dtzr?((*D[[7 hj 
 KK 2 KKO

 ~!1 4 	

c 
f &))S  	X17VWW 
	X 
sB   +FE>.F,FBFF>FFFF
F+c           
         [        5         [        U =(       d    0 5      n[        UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       nU(       d  [        S5      e[        UR                  S5      =(       d    S5      R	                  5       =(       d    Sn[        UR                  S5      =(       d    [        U5      5      R	                  5       n[        5       n[        5        nUR                  S5        UnUnS	n	UR                  S
U45      R                  5       (       a4  U SU	 3nU	S-  n	UR                  S
U45      R                  5       (       a  M4  Un[        XdX#5      n
UR                  S5      R                  5       nU(       a   [        R                  " US   5      nO0 nUR                  S/ 5        UR                  S0 5        UR                  S0 5        UR                  S/ 5        UR                  S/ 5        X<S   ;  a  US   R!                  U5        X<S   U'   SUS   U'   UR                  S/ 5       Vs/ s H  n[        U5      U:w  d  M  UPM     snUS'   / US'   UR                  S[        R"                  " USSS9XU45        UR%                  5         SSS5        SW
UUUSS.S.$ ! [         a    0 n GNf = fs  snf ! , (       d  f       N1= f)z
Create a graph/container physically in SQLite.

Policy:
- A graph is a container/row.
- Creating a graph must NOT create timeline events.
- Timeline components are created later by manual event insertion or API ingest.
r   r   rY   zgraph name is requiredr   r   r   r/  ro   r   r  rp   z5SELECT value_json FROM ui_state WHERE key = 'folders'rX  r   r[  ry  rY  rZ  Tr  Frb  Nr   )r   r   r   r   events_created)r2  r   )r.   r[   r=   r\   rK   r!  r  r   r*   r'   r   r   rf  rg  rM   r   rq   ri  r-   )r   r3  r   r   r   r   r)   base	candidater   r   r   r  r   s                 r   create_graph_sqlr    s    2DtxxA488N#;ArBHHJD122dhhx(;O<BBDWK$((9%@):4)@AGGIG	B	%&	kkCi\R[[]]&!IFA kkCi\R[[]] &sTGkkC

(* 	 

3|#45 EB'+$+r*I..)##K0(3mW%%)fk"+099]B+G]+Ga3q6U\K\+G]m l ZZETBBK		
 	

i 
n  !
	 	A     ^M 
sK   A0K)44K))KBK)K$5K$;AK)K!K) K!!K))
K7c                  
   [        5         SSKn SnSn[        5       n[        5        nUR	                  S5      R                  5       nU(       d  SSSS.sSSS5        $  U R                  " US   =(       d    S5      n[        U[        5      (       d  SSS	S.sSSS5        $ UR	                  S
5        UR                  5        GH  u  p[        U	[        5      (       d  US-  nM"  [        U=(       d    S5      n
U
R                  SS5      n[        U5      S:  a  US   R                  5       OSnSnSnSn[        U5      S:  a0   [        US   5      n[        US   5      nUS   R                  5       n[        U	R!                  S5      =(       d;    U	R!                  S5      =(       d#    U=(       d    U	R!                  S5      =(       d    S5      R                  5       n[        U	R!                  S5      =(       d#    U	R!                  S5      =(       d    U=(       d    S5      R                  5       n[        U	R!                  S5      =(       d    U	R!                  S5      =(       d    S5      R                  5       n[        U	R!                  S5      =(       d    U	R!                  S5      =(       d    S5      R                  5       n[        U	R!                  S5      =(       d    U	R!                  S5      =(       d    S5      R                  5       n[        U	R!                  S5      =(       dJ    U	R!                  S5      =(       d2    U	R!                  S5      =(       d    U	R!                  S5      =(       d    S5      R                  5       n[        U	R!                  S 5      =(       d    U	R!                  S!5      =(       d    S5      R                  5       nU(       a  U(       d  US-  nGM   SnUb&  Ub#  UR	                  S"UXU45      R                  5       nUc*  U(       a#  UR	                  S#UUU45      R                  5       nUc"  UR	                  S$UU45      R                  5       nUc  US-  nGM  US   nUS%   nUS&   nUS'   nU(       a   [#        U5      u  nnnnUR	                  S(UUUUUUUUUU[        US)   5      45        US-  nGM     UR%                  5         SSS5        XS*.$ ! [         a"  nSS[        U5      S.s SnAsSSS5        $ SnAff = f! [         a    SnSn GNf = f! [         a     Nf = f! , (       d  f       Nf= f)+a  
Synchronize UI technical-card metadata into canonical events.

Source:
- ui_state key = 'event_meta'

Target:
- events.place
- events.who
- events.brief_description
- events.source_link
- events.event_name
- events.date_text/date_year/date_month/date_day

This function does not create events. It only updates existing events.
Matching uses:
1. event_meta key format row:start:end:name when available.
2. saved group/name/date fields.
3. row_key + event_name fallback.
r   Nz6SELECT value_json FROM ui_state WHERE key='event_meta'zno event_meta)rF  r{  rv  rX  z{})rF  r{  r  zevent_meta is not objectr/  rp   rY   r1  r   r   ro   r   r   r   r   r   r   r   r~   r   r   r   r   r   r   r   r   r   ai  
                    SELECT id, event_name, date_text, date_year, date_month, date_day
                    FROM events
                    WHERE row_key = ?
                      AND start_col = ?
                      AND end_col = ?
                      AND event_name = ?
                    ORDER BY id DESC
                    LIMIT 1
                    aC  
                    SELECT id, event_name, date_text, date_year, date_month, date_day
                    FROM events
                    WHERE row_key = ?
                      AND event_name = ?
                      AND date_text = ?
                    ORDER BY id DESC
                    LIMIT 1
                    a  
                    SELECT id, event_name, date_text, date_year, date_month, date_day
                    FROM events
                    WHERE row_key = ?
                      AND event_name = ?
                    ORDER BY id DESC
                    LIMIT 1
                    r   r   r   a  
                UPDATE events
                SET event_name = ?,
                    date_text = ?,
                    date_year = ?,
                    date_month = ?,
                    date_day = ?,
                    place = ?,
                    who = ?,
                    brief_description = ?,
                    source_link = ?,
                    updated_at = ?
                WHERE id = ?
                r   )rF  r{  )r.   rf  r   r*   r'   r   rg  rM   r=   rZ   r[   r~  splitrt   rK   r   r\   r+  r-   )rf  rF  r{  r   r)   r   metaexcr`   rS   key_spartskey_row	key_startkey_endkey_namer   r   r4  r~   r   r   r   rB   r   r   r   r   s                               r   sync_event_meta_to_events_sqlr  ,  s   * GG	B	kkD

(* 	  Q/J 
	C::c,/748D $%% Q:TU 
  	%&**,JCeT**1	rNEKKQ'E*-e*/eAhnn&rGIGH5zQ# #E!HI!%(mG !8>>+		%  99Y' 99W%  eg  		&! 99\* 	
 eg  		&! 99[) eg	  		'*Feii.@FBGMMOEeii&@%))E*:@bAGGIC #		-. 99W%99W% 99]+ ! eg  eii6P%))E:JPbQWWYK*1F$)<	 i*= (*  ~( j(3 (*  ~ j)
 (*  ~1{+I{+I-Jj)HAQRZA[>Iy*h KK %t%: qLGe 'h 	

M 
P 33}  	C QSBB 
	C2 ! # $I"G#B ! I 
s   ,T8S$<T8 B"T8TL%T8T(AT8$
T.T<T=T8TT8T%!T8$T%%T8(
T52T84T55T88
Uc                     [        5         [        5       n [        5         [        5        nUR	                  S5      R                  5       S   nUR	                  S5      R                  5       S   nUR	                  S5      R                  5       S   nSn UR	                  S5      R                  5       S   nSSS5        [        5         WS:H  UWWWS.$ ! [         a    Sn N+f = f! , (       d  f       N5= f)	a>  
Explicit session save endpoint.

Operational policy:
- Does not create data.
- Does not delete data.
- Forces frontend render from canonical SQLite.
- Verifies SQLite integrity.
- Returns current object counts.

Most operations already persist transactionally. This endpoint is the
explicit user-facing Save barrier.
PRAGMA integrity_checkr   SELECT COUNT(*) FROM graphsSELECT COUNT(*) FROM events!SELECT COUNT(*) FROM api_registryNr2  )r2  sqlite_integrityrw  eventsapis)r.   r  r  r*   r'   r   rM   rH   )event_meta_syncr)   	integrityr  event_count	api_counts         r   save_session_sqlr    s     35O	KK 89BBDQG	kk"?@IIKANkk"?@IIKAN		$GHQQSTUVI 
  4%   	I	 
s*   A)C$"CC!C$ C!!C$$
C2c                      [        5         [        5        n U R                  S5        U R                  S[        5       45        U R	                  5         S S S 5        g ! , (       d  f       g = f)Na\  
            CREATE TABLE IF NOT EXISTS persistence_audit(
                id INTEGER PRIMARY KEY CHECK(id = 1),
                revision INTEGER NOT NULL DEFAULT 0,
                dirty INTEGER NOT NULL DEFAULT 0,
                last_operation TEXT,
                last_status TEXT,
                last_error TEXT,
                sqlite_integrity TEXT,
                graph_count INTEGER DEFAULT 0,
                event_count INTEGER DEFAULT 0,
                folder_count INTEGER DEFAULT 0,
                api_count INTEGER DEFAULT 0,
                updated_at TEXT
            )
            z
            INSERT OR IGNORE INTO persistence_audit(
                id, revision, dirty, last_operation, last_status,
                sqlite_integrity, updated_at
            )
            VALUES (1, 0, 0, 'init', 'ok', 'unknown', ?)
            )r.   r*   r'   r   r-   r(   s    r   ensure_persistence_audit_schemar    sQ    		
$ 	 YL		
 	

; 
s   =A
A)c                 f   U R                  S5      R                  5       S   nU R                  S5      R                  5       S   n U R                  S5      R                  5       S   n U R                  S5      R                  5       S   nXX44$ ! [         a    Sn N5f = f! [         a    Sn N$f = f)Nr  r   r  zSELECT COUNT(*) FROM foldersr  )r'   r   rM   )r)   r  r  folder_countr  s        r   persistence_counts_sqlr  @  s    ++;<EEGJK++;<EEGJK{{#ABKKMaPKK CDMMOPQR	 \<<  
  	s$   "B )"B! BB!B0/B0c                    [        5         [        5        nUR                  S5      R                  5       S   n[	        U5      u  pgpUS:H  =(       a    US:H  n
[        5       nUR                  SU(       a  SOSU U
(       a  SOS[        U=(       d    S5      UUUUU	U4
5        UR                  5         S S S 5        W
[        U5      U U
(       a  SOS[        U=(       d    S5      WWWWW	WS.$ ! , (       d  f       N@= f)	Nr  r   r2  a  
            UPDATE persistence_audit
            SET revision = revision + 1,
                dirty = ?,
                last_operation = ?,
                last_status = ?,
                last_error = ?,
                sqlite_integrity = ?,
                graph_count = ?,
                event_count = ?,
                folder_count = ?,
                api_count = ?,
                updated_at = ?
            WHERE id = 1
            rp   r  rY   )r2  r9  	operationr:  r  r  rw  r  r   r  r   )	r  r*   r'   r   r  r   r=   r-   bool)r  r9  r:  r  r)   r  r  r  r  r  r2  r   s               r   r<  r<  Q  s    #%	KK 89BBDQG	<RSV<W9,$16T>Y  EKR !	
: 	

G 
L e$'U[b!% K 
s   BC++
C9c                  T   [        5         [        5        n U R                  S5      R                  5       S   n[	        U 5      u  p#pEU R                  S5      R                  5       nU(       a  [        U5      O0 nUS:H  =(       a    UR                  SS5      S:H  [        UR                  SS5      5      UR                  SS5      UR                  SS	5      UR                  SS	5      UR                  S
S	5      UUUUUUR                  SS	5      S.sS S S 5        $ ! , (       d  f       g = f)Nr  r   z,SELECT * FROM persistence_audit WHERE id = 1r2  last_statusr9  revisionlast_operationrY   
last_errorr   )r2  r9  r  r  r  r  r  rw  r  r   r  r   )r  r*   r'   r   r  r[   r\   r  )r)   r  r  r  r  r  r   r3  s           r   persistence_status_sqlr    s    #%	KK 89BBDQG	<RSV<W9,kkHIRRTtCyR t#M(E(M$((7A./Q/"hh'7<88M26((<4 )!!#((<4
 
s   C:D
D'c                 `   [        5         [        U =(       d    0 5      n[        UR                  S5      =(       d    UR                  S5      =(       d    S5      R	                  5       =(       d    SnSU;   a  US   nOSU;   a  US   nOSU;   a  US   nOUn[        U[        5      (       a   [        R                  " U5      n[        U[        5      (       d  SU0n[        5       n[        5        nUR                  S5        US:X  Ga  UR                  S/ 5        UR                  S	0 5        UR                  S
0 5        UR                  S/ 5        UR                  S/ 5        UR                  S/ 5      =(       d    /  Hn  n[        U5      R	                  5       nU(       d  M%  [        XV5      n UR                  SUR                  S
0 5      R                  US5      (       a  SOSXG45        Mp     UR                  S	0 5      =(       d    0 R                  5        H  u  p[        U5      R	                  5       n[        U5      R	                  5       =(       d    SnUR                  SU45      R                  5       n	U	(       d  Mj  [        XV5      nUR                  SXtU45        M     UR                  SU[        R                   " USSS9XD45        UR#                  5         SSS5        [%        SSSS9  SUSS.$ ! [         a    SU0n GNFf = f! [         a     GM  f = f! , (       d  f       NF= f)z
Persist current UI folder state into SQLite.

This is required for autosave of folders, graph-folder mappings,
open/closed folder state, and deleted-row state.
r`   	state_keyr   rS   r3  rX  rT   r/  r[  ry  rY  rZ  z;UPDATE folders SET is_open = ?, updated_at = ? WHERE id = ?Trp   r   r   r   zAUPDATE graphs SET folder_id = ?, updated_at = ? WHERE row_key = ?a  
            INSERT INTO ui_state(key, value_json, created_at, updated_at)
            VALUES (?, ?, ?, ?)
            ON CONFLICT(key) DO UPDATE SET
                value_json=excluded.value_json,
                updated_at=excluded.updated_at
            Frb  Nzui-stater2  r8  )r2  r`   rC  )r.   r[   r=   r\   rK   rZ   rf  rg  rM   r   r*   r'   r   r   r~  r   ri  r-   r<  )
r   r3  r`   rS   r   r)   r   r   r   r7   s
             r   save_ui_state_payload_sqlr    s    2D
dhhuoC+!6C)
D
J
J
L
YPYC$W	4V		\"%	#JJu%E eT""% 	B	%&)Y+]B/VR(]B/\2.$yyB7=2=!+.446"0B	KKU#ii377TJJPQSUa  > */=")E)K(R(R(T$g,,,.!+.446I/=J (* 
 0B	WG, )U& 	 $**U$GP		
 	

o 
r Zq>sT22E  	#ENE	#< ! - 
sD   -K8 0B>L/=L,C3L8L
	L

LLLL
L-c           	         [        U =(       d    0 5      n  U R                  S5      (       a  [        SU R                  S5      S.5        [        5         [	        SSSS9$ ! [
         a  n[	        SSS	[        U5      S
9s SnA$ SnAff = f)z
Autosave barrier.

It first persists UI state when present, then verifies SQLite and regenerates
the HTML. It does not fabricate application data.
ui_stater   )r`   rS   autosaver   r2  r8  rp   r  )r9  r:  r  N)r[   r\   r  rH   r<  rM   r=   )r   r  s     r   autosave_session_sqlr    s     7=b!G
;;z""%  Z0' 
 	'
!DII 
'c(	
 	

s   AA 
B(B<BBc                      [        0 5      $ )zC
Manual Save button uses the same real autosave verification path.
)r  r   r   r   r  r    s      ##r   c                     [        5         SSSSSSSSSSSSSSSS.n [        5        nUR                  S5      R                  5        Vs/ s H  nUS	   PM
     nnU(       d  / sSSS5        $ S
R	                  U5      nUR                  SU S35      R                  5       n/ nU H  n[        U5      nU R                  5        H  u  pUR                  X5        M     SU;  a5  UR                  S	5      =(       d    UR                  S5      =(       d    SUS'   [        UR                  S5      5      US'   UR                  U5        M     UR                  S S9  UsSSS5        $ s  snf ! , (       d  f       g= f)a  
Schema-tolerant API registry reader.

It reads the actual api_registry columns present in SQLite and fills
missing operational fields with safe defaults. This prevents /api/health
from failing when older databases do not yet contain last_status,
last_latency_ms, last_run_at, etc.
Nr   unknownrY   TGET)r  last_latency_mslast_run_atlast_insertedlast_updatedlast_skippedr:  raw_jsonr   r   enabledmethodtarget_graphendpointr   PRAGMA table_info(api_registry)r   , zSELECT z FROM api_registryr   r  r   r  c                     [        U R                  S5      =(       d    S5      [        U R                  S5      =(       d    S5      4$ )Nr   rY   r   )r=   r\   )r   s    r   <lambda>list_apis.<locals>.<lambda>[  s-    AEE&M$7R 8#aeeDk>OR:PQr   )r`   )r.   r*   r'   re  r  r[   r~  r   r\   r  rq   sort)
wanted_defaultsr)   r  colsselect_colsrl  r  ru   r`   r_   s
             r   	list_apisr  "  sz     O$ 
 [[!BCLLN
N fIN 	 

  
 iio{{k]"45

(* 	 A7D / 5 5 7- !8 4!XXf-N*1ENT
"488I#67DOJJt  	QR; 

 
s$   "E)	E$
E),C.E)$E))
E7c                 @   [        5         [        U =(       d    0 5      n[        UR                  S5      =(       d%    S[        R
                  " 5       R                  S S  35      n[        UR                  S5      =(       d    U5      nUR                  SS5      (       a  SOSn[        UR                  S	5      =(       d    UR                  S
5      =(       d    S5      n[        UR                  S5      =(       d    S5      R                  5       n[        UR                  S5      =(       d    S5      n[        UR                  S5      =(       d    S5      n[        UR                  S5      =(       d    S5      n	UU[        U5      UUUUU	S.UEn
[        5       n[        5        nUR                  S5        UR                  SX#XEXgX[        R                  " U
SSS9X45        U(       a  [        XUS5        UR!                  5         S S S 5        U
$ ! , (       d  f       U
$ = f)Nr   API_r  r   r  Trp   r   r  r   rY   r  r  r  interval_seconds,  r:  
registered)r   r   r  r  r  r  r  r:  r/  a  
            INSERT INTO api_registry(id, name, enabled, target_graph, method, endpoint,
                                     interval_seconds, status, raw_json, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            ON CONFLICT(id) DO UPDATE SET
                name=excluded.name,
                enabled=excluded.enabled,
                target_graph=excluded.target_graph,
                method=excluded.method,
                endpoint=excluded.endpoint,
                interval_seconds=excluded.interval_seconds,
                status=excluded.status,
                raw_json=excluded.raw_json,
                updated_at=excluded.updated_at
            Frb  r   )r.   r[   r=   r\   r   r   hexr   r   r  r   r*   r'   rf  ri  r   r-   )r   r3  r  r   r  r  r  r  intervalr:  rT   r   r)   s                r   
upsert_apir  `  s   2D$AT$**,*:*:3B*?)@#ABFtxx)6*D88It,,a!Gtxx/J488G3DJKL(#,u-335F488J'-2.H488./637H(#3|4F=$$
 
C 
B	%& 7&HZZ%4@"J!	
& <Q

/ 
0 J1 
0 Js   "A"H
Hc                       [        5        n U R                  S5      R                  5        Vs/ s H  nUS   PM
     snsS S S 5        $ s  snf ! , (       d  f       g = f! [         a    / s $ f = f)Nr  r   )r*   r'   re  rM   )r)   r  s     r   api_registry_columnsr    sk    \S %FGPPRRA &	R \ \
  	s=   
A "AA	A	A 	A
AA A A.-A.c                     U (       d  0 $ [        U [        5      (       a  U $ [        U [        5      (       a0   [        R                  " U 5      n[        U[        5      (       a  U$ 0 $ 0 $ ! [
         a    0 s $ f = fr0   )rZ   r[   r=   rf  rg  rM   )rS   parseds     r   parse_json_objectr     sp    	%%	ZZ&F'556=2= I  	I	s   ,A( $A( (A76A7c                    0 n[        U [        R                  5      (       a  [        U 5      n [        U [        5      (       a<  [	        U R                  S5      5      nUR                  U5        UR                  U 5        UR                  SUR                  S5      =(       d    UR                  S5      =(       d    S5        UR                  SUR                  S5      5        UR                  SS5        UR                  SS	5        UR                  SUR                  S
5      =(       d    S5        UR                  SUR                  S5      =(       d    UR                  S5      =(       d    S5        UR                  SUR                  S5      =(       d    S5        UR                  SS5        UR                  SS5        UR                  SS5        [        UR                  S5      5      US'   U$ )zw
Normaliza una fila de api_registry mezclando columnas reales y raw_json.
Esto permite soportar bases viejas y nuevas.
r  r   r   r  r   r  Tr  r  r   rY   r  r   r   r:  r  r  r  Nr  r  )	rZ   r"   r%   r[   r   r\   rQ  r   r  )r   ru   rT   s      r   normalize_api_rowr    s^   
 D#w{{##3i#t
 34CCOOD$((6*Kdhhz.BKeLOOFDHHTN+OOIt$OOHe$OOJ 526OONDHHW$5$N%$NBOOOHdhh}5BCOO%t,OOM2&OOL"%488I./DOKr   c           
         [        5         [        U 5      n [        5       nU(       d  SSS.$ [        5       n[	        5        nUR                  S5        UR                  SX 45      R                  5       nU(       d  UR                  5         SSU S.sSSS5        $ 0 nUR                  5        H  u  pxXr;   d  M  XU'   M     S	U;   a  X6S	'   U(       ah  S
R                  UR                  5        V	s/ s H  o S3PM	     sn	5      n
[        UR                  5       5      US   /-   nUR                  SU
 S3U5        SU;   ad  [        SUR                  5       ;   a  US   OS5      nUR                  U5        X<S	'   UR                  S[        R                   " USSS9US   45        UR#                  5         SSS5        SU SS.$ s  sn	f ! , (       d  f       N= f)u   
Actualiza estado runtime de una API de forma tolerante al esquema.
Si algunas columnas no existen, las ignora.
También intenta fusionar raw_json cuando existe.
Fz!api_registry table has no columnsr2  r  r/  zISELECT * FROM api_registry WHERE CAST(id AS TEXT) = ? OR name = ? LIMIT 1zapi not found)r2  r  r  Nr   r  z = ?r   zUPDATE api_registry SET z WHERE id = ?r  rY   z1UPDATE api_registry SET raw_json = ? WHERE id = ?Trb  )r2  r  rF  )r.   r=   r  r   r*   r'   r   rI  r~  r  r^   rr   rL  r   rQ  rf  ri  r-   )r  fieldsr  r   r)   r   directr`   rS   rn  assignmentsparamsrT   s                r   set_api_statusr	    s    [F!D&IJJ	B	%&kkW
 (* 	
 LLN/VL 
  ,,.JC{#s ) 4#%< ))$GAs$Z$GHK&--/*c$i[8FKK*;-}E
 #zSXXZ7OC
OUWXCJJv "KKCCetDc$iP
 	

O 
R &T::' %H- 
s,    AGG76G-G 
;B7G G
Gc                 6   SSS.nU R                  S5      =(       d2    U R                  S5      =(       d    U R                  S5      =(       d    Sn[        U5      nUR                  5        H*  u  pEU(       d  M  Uc  M  [        U5      U[        U5      '   M,     U$ )NzGRAFO-D-V55-API-SENSOR/1.0z!application/json, text/plain, */*)z
User-AgentAcceptheaders_jsonheadersrequest_headersrY   )r\   r   r~  r=   )r   r  raw_headersr  r`   rS   s         r   request_headers_for_apir    s    25G 	 	779	77$%	 	  {+Flln
35$ #E
GCH % Nr   c                     [        U R                  S5      =(       d2    U R                  S5      =(       d    U R                  S5      =(       d    S5      R                  5       $ )Nr  r   urirY   )r=   r\   rK   )r   s    r   endpoint_for_apir  !  sN    
 	775>	775>	 	
 egr   c                     [        U R                  S5      =(       d    S5      R                  5       R                  5       nUS;  a  gU$ )Nr  r  )r  HEAD)r=   r\   r   rK   )r   r  s     r   safe_api_methodr  *  s>    "+e,224::<F _$Mr   c                    [        U 5      n [        U R                  S5      =(       d    U R                  S5      =(       d    S5      n[        U R                  S5      =(       d    U5      n[        U 5      nUU[	        U R                  S5      5      [        U R                  S5      =(       d2    U R                  S5      =(       d    U R                  S5      =(       d    S5      U[        U 5      S	S	S
S
[        5       S
S.nU R                  S5      (       d  S	US'   S	US'   SUS'   U$ U(       d  S	US'   S	US'   SUS'   U$ [        U5      nUR                  S;  a  SUS'   SUS'   SUS'   U$ [        R                  " 5       n [        U[        U 5      [        U 5      S9n[        XS9 n	[        [        R                  " 5       U-
  S-  5      n
[        [        U	SS5      =(       d    U	R!                  5       =(       d    S5      nXS'   XS'   SUs=::  a  S:  a  O  OSUS'   SUS'   S
US'   O5SUs=::  a  S:  a  O  OSUS'   SUS'   SU 3US'   OSUS'   SUS'   S U 3US'   S
S
S
5         [)        UUS   US   US   US$   US   =(       d    SUS$   US   S%9  U$ ! , (       d  f       N?= f! ["         ai  n[        [        R                  " 5       U-
  S-  5      n
[        [        US!S5      =(       d    S5      US'   XS'   SUS'   SUS'   S"US    3US'    S
nANS
nAf[$         aV  n[        [        R                  " 5       U-
  S-  5      n
XS'   SUS'   SUS'   [        [        US#U5      5      US'    S
nAGNS
nAf[&         aK  n[        [        R                  " 5       U-
  S-  5      n
XS'   SUS'   SUS'   [        U5      US'    S
nAGNaS
nAff = f! [&         a     U$ f = f)&z
Verifica conectividad real de una API registrada.

Estados:
- active: HTTP 2xx
- degraded: HTTP 3xx o respuesta rara pero alcanzable
- inactive: timeout, error de red, HTTP 4xx/5xx
- unknown: API deshabilitada o sin endpoint
r   r   r   r  r  r   r   rY   r  N)r   r   r  r  r  r  r:  runtime_statushttp_status
latency_mslast_checkedr  r:  r  API disabledr  Missing endpointhttphttpsinactiveUnsupported endpoint schemer  r  r      r   r  r     r  activer  degradedzRedirect/status HTTP codez
HTTPError rv  r  r:  r  r  r  r  r  r  )r  r=   r\   r  r  r  r   r   schemer   perf_counterr
   r  r   r   getattrgetcoder   r   rM   r	  )r   r!   r  r   r  r  r  startedreqreslatencyr  r  s                r   check_api_connectivityr3  4  s    C
 C:#''&/:U;Fswwv(&)D$H 	*+CGGN3_swww7G_377SX>_]_`!#&#	D 779"X!*&W"X!**WhF}}--#X!+5W!G/!+C0"3'
 S*c4,,.8D@AGgc8Q7M3;;=MANK"-!(k'C'!)X)1%& $W)c)!+X)3%&"2;- @W!+X)3%&"'} 5W% +T>]+ .^,G}*n- 01		
 Kq +*(  ;t((*W4<=!'#vq"9">Q?]$\#X!+$T-%8$9:W 9t((*W4<=$\#X!+GC378W !t((*W4<=$\#X!+CW!$  Ksd   ?&J' %B8JJ' &.O 
J$ J' $J' '
O1ALO"AM33O A OO
OOc           	         U(       d  g[        U5      R                  5       n[        U R                  S5      =(       d    S5      [        U R                  S5      =(       d    S5      [        U R                  S5      =(       d    S5      [        U R                  S5      =(       d    S5      [        U R                  S5      =(       d    S5      1nX;   $ )NTr  rY   r   r   r   r   )r=   rK   r\   )r   r   rv   s      r   api_matches_graphr5    s    JE 	CGGN#)r*CGGG"#CGGEN b!CGGFO!r"CGGDMR J r   c                    [        5         [        5        Vs/ s H  n[        U5      PM     nnU Vs/ s H  n[        X5      (       d  M  UPM     nnU Vs/ s H  n[	        U5      PM     nnU(       d  SnOP[        S U 5       5      (       a  SnO6[        S U 5       5      (       a  SnO[        S U 5       5      (       a  SnOSnSU U[        5       [        U5      US	.$ s  snf s  snf s  snf )
z[
Endpoint backend para el sensor real:
GET /api/apis/status
GET /api/apis/status?graph=ISS
r  c              3   0   #    U  H  oS    S:H  v   M     g7fr:  r!  Nr   r^  r   s     r   r`  !api_status_sql.<locals>.<genexpr>       81x[J&   r!  c              3   0   #    U  H  oS    S:H  v   M     g7fr:  r'  Nr   r9  s     r   r`  r:    r;  r<  r'  c              3   0   #    U  H  oS    S:H  v   M     g7fr:  r  Nr   r9  s     r   r`  r:         7w!x[I%wr<  r&  Tr2  r   r:  
checked_atcountr  r.   r  r  r5  r3  rh  r   rt   r   r   r  selectedresultsoveralls         r   api_status_sqlrJ    s     ;C 	# 	  S( 	t   C 	s#  
 	88	8	8	88	8	8	7w7	7	7 iW 5

s   C!C&
C&C+c                    [        U 5      n [        U 5      nU(       d  [        S5      e[        U5      nUR                  S;  a  [        S5      e[        U[        U 5      [        U 5      S9n[        XAS9 nUR                  S5      nSSS5        WR                  SS	S
9n [        R                  " U5      $ ! , (       d  f       N5= f! [         a    SU0s $ f = f)z2
Descarga JSON desde la API. Se usa por sync_api.
zAPI endpoint is requiredr  r"  r#  r    i   Nutf-8r&  )errorstext)r  r  r!  r   r+  r
   r  r  r   readdecoderf  rg  rM   )r   r!   r  r  r0  r1  rT   rN  s           r   fetch_api_jsonrQ    s     C
 C$H344hF}}--677
',s#C 
	&#hh{# 
' ::gi:0Dzz$ 
'	&  ~s   5B6 C 6
CCCc                     [        U [        5      (       a  U $ [        U [        5      (       a6  S H-  nU R                  U5      n[        U[        5      (       d  M+  Us  $    U /$ SU 0/$ )z=
Convierte respuestas comunes de APIs en una lista de items.
)r  r~  rH  r3  r   rS   )rZ   rr   r[   r\   )r   r`   rw   s      r   payload_itemsrS  	  sf     '4  '4  DC++c"C#t$$
 E
 ygr   c                    [        U 5      n [        U[        5      (       d  SU0n[        U R	                  S5      =(       dJ    U R	                  S5      =(       d2    U R	                  S5      =(       d    U R	                  S5      =(       d    S5      n[        U R	                  S5      =(       d    U R	                  S5      =(       d    S5      n[        U5      u  pV[        USS	5      n[        U5      nU(       a  Uu  ppnU S
U	 3nU
nO=[        [        USU SUS-    35      5      n[        U=(       d    [        5       SS 5      n[        X5      (       a*  U(       a	  SUS    3nOSU;   a  SUR	                  S5       3n[        XU5      n[        [        USS	5      5      nU(       d   [        R                  " USSS9n[        [        US[        R                  " [        R                  " USSS9R!                  S5      5      R#                  5       SS 5      5      nU R	                  S5      =(       d    U SU 3UUUUUUU[        [        USU5      5      UU[%        U 5      UUS.$ ! [         a    [        U5      n Nf = f)u3   
Mapeo tolerante API → evento canónico GRAFO_D.
rS   r  r   r   r   APIr   )r   r   r   r   r   rP   rY   z
 position )r   r   r   r   z event rp   Nr  zISS position r   r   )r   r   r   summaryrN  messageFTrb  )r   r   r   r   rL     r1  )r   r   r.  )r   r   r   r   r   r   r   r~   r   r   r   r   r   r   )r  rZ   r[   r=   r\   r|   ra   rW   r   r   r   rf  ri  rM   hashlibsha1encode	hexdigestr  )r   ru   indexr  r   rj   rl   raw_timer   r   r   rP   rQ   rR   r   
event_dater~   r   	source_ids                      r   event_from_api_itemra  	  s|    C
 CdD!! 	777	775>	 776?	 L 3776?<cggdm<u=Ht$HCF
H x(E491c:l^4
8*GEAI;/
 3WYs^4
c  "58*-DD "488K#8"9:D3'EW	
K 	$**TNK 3LL

4etDKKGTik#2	
I *(+1YK8='A8LM"(',   	$d)K	$s   (I' 'I?>I?c                    [        5         [        U 5      n [        5        Vs/ s H  n[        U5      PM     nnSnU HC  n[        UR	                  S5      5      U :X  d   [        UR	                  S5      5      U :X  d  MA  Un  O   U(       d  SU SS.$ [        U[        S9nUS   S	;  a"  SU US
S
S
UR	                  S5      =(       d    SS.$ [        U[        S9n[        U5      nS
nS
nS
n	[        U5       H  u  p [        XU
S9n[        USS9nUR	                  S5      nUS:X  a  US-  nOUS:X  a  US-  nOU	S-  n	 [        5        nUR                  S[        UR	                  S5      =(       d    U 5      [        UR	                  S5      5      [        U=(       d    S5      [        5       45        UR                  5         SSS5        M      [#        U UUU	S[        5       S9  [%        5         SU UUUU	S.$ s  snf ! , (       d  f       GM  = f! [          a     GM)  f = f! [          a
    U	S-  n	 GM@  f = f! [          a     N`f = f)u   
Sincronización tolerante de una API registrada.

- Verifica conectividad real.
- Si responde, intenta leer JSON.
- Normaliza items a eventos.
- Inserta o actualiza eventos en SQLite.
Nr   r   FzAPI not found)r2  r  r  r    r:  )r&  r'  r   r  zAPI not reachable)r2  r  r:  rG  rF  r{  r  )r]  TrJ  rH  rG  rp   rF  z
                        INSERT OR IGNORE INTO api_ingest_log(
                            api_id, event_legacy_id, action, created_at
                        )
                        VALUES (?, ?, ?, ?)
                        upsertr&  )r  r  r  r:  r  )r2  r  r:  rG  rF  r{  )r.   r=   r  r  r\   r3  API_SYNC_TIMEOUT_SECONDSrQ  rS  	enumeratera  rN  r*   r'   r   r-   rM   r	  rH   )r  r   r  r  r:  r   r~  rG  rF  r{  r]  ru   r   resultrH  r)   s                   r   sync_apirh  v	  su    [F ;C 	# 	 
 C	y}}T"#v-Y]]65J1Kv1UC 
 $
 	
 $C1IJFh55ZZ(?,?
 	
 S*BCG'"EHGG '"	'?E%e$?FZZ)F#A9$11\SKK   78		$0 2(3#I	 JJL "\ (J
"  		
   ul "\     	qLG	  sg   G7:AH!=
HA:G<HH8 <
H	HH
HH!HH!!H54H58
IIc                  l     [        5       n [        R                  " 5       nU  H  nUR                  S5      (       d  M  UR                  S5      nU(       d  M5  [        UR                  S5      =(       d    S5      nUR                  S5      nSn[	        U[        [
        45      (       a  U[        U5      -
  U:  nU(       d  M  [        US   XR                  S5      =(       d    S	S
9   [        US   5        M     [        R                  " S5        GM  ! [         a     M  f = f! [         a     N6f = f)NTr  r  r  r  last_run_epochr   r:  	scheduled)rj  r:  r   )
r  r   r\   r   rZ   rL   r	  rh  rM   sleep)r  nowr   r  r  last_rundues          r   api_poll_workerrp  	  s   
	;D))+Cwwy))77:.sww'9:AcB77#34he55h/8;C3"3t9SQYIZIi^ij T+ ( 	

11 ( %  		s<   B=D& %D& *D8D& 
D#D& "D##D& &
D32D3c                  r    [         (       a  g Sq [        R                  " [        SS9n U R	                  5         g )NTrA   )_api_worker_startedrD   rE   rp  rF   )ts    r   start_api_worker_oncert  
  s,    =AGGIr   c                   P   ^  \ rS rSrS
S jrS rU 4S jrS rS rS r	S r
S	rU =r$ )Handleri
  c                 2   [         R                  " USSS9R                  S5      nU R                  U5        U R	                  SS5        U R	                  S[        [        U5      5      5        U R                  5         U R                  R                  U5        g )NFro   )rc  indentrL  zContent-Typezapplication/json; charset=utf-8Content-Length)
rf  ri  r[  send_responsesend_headerr=   rt   end_headerswfilewrite)selfobjr)  r3  s       r   
_send_jsonHandler._send_json
  su    zz#E!<CCGL4 )JK)3s4y>:

r   c                     [        U R                  R                  SS5      5      nU(       a>  [        R                  " U R
                  R                  U5      R                  S5      5      $ 0 $ )Nry  0rL  )r   r  r\   rf  rg  rfilerO  rP  )r  r   s     r   _payloadHandler._payload
  sL      !1378ABtzz$**//!,33G<=JJr   c           	        > [        U R                  5      nUR                  n[        UR                  5      n US;   a  [	        5       nU R                  U5        g US:X  a  U R                  [        5       5        g US:X  aI  SnUR                  S5      (       a  UR                  SS/5      S   n[        US9nU R                  U5        g [        5       nUS:X  a  U R                  U5        g US	:X  a  [        5       nU R                  U5        g US
:X  a"  U R                  UR                  S/ 5      5        g US:X  a"  U R                  UR                  S/ 5      5        g US:X  a"  U R                  UR                  S/ 5      5        g US:X  a  UR                  S/ 5       Vs/ s H%  n[        UR                  S5      5      S:w  d  M#  UPM'     nnU R                  S[        [        5      [        U5      [        [        5       5      U R                  5       S.5        g  [         T	U ]E  5       $ s  snf ! [         a(  nU R                  S[        U5      S.S5         S nAg S nAff = f)N)z/api/autosave/statusz/api/persistence/status	/api/apisz/api/apis/statusrY   r   r   )r   z/api/db/api/session/save/api/eventsr  /api/graphsrw  z/api/alertsalertsz/api/healthr   __AXIS__T)r2  dbr  r  r  Fr    )r   pathr	   queryr  r  r  r\   rJ  r1   r  r=   r$   rt   r  rM   superdo_GET)
r  
parsed_urlr  r  rg  r   r  egraph_events	__class__s
            r   r  Handler.do_GET
  s&   dii(
))*9	JJ/1'{"	,))99W%%!IIgt4Q7E'e4'By #**)+'}$x 45}$x 45}$x 45}$!vvh3 3!155<(J6 3    b'!,/	,(,(=(=(?!   %$ w~#   	OO53q6:C@	sa   !H/ H/ ;AH/ 
!H/ ,!H/ 'H/ 6'H/ 'H/ H/  "H*H*AH/ *H/ /
I!9II!c                     [        5        nUR                  S5      R                  5       S   sS S S 5        $ ! , (       d  f       g = f)Nr  r   )r*   r'   r   )r  r)   s     r   r  Handler.sqlite_integrity^
  s.    \S;;78AACAF \\s	   "7
Ac                 l   [        U R                  5      R                  n U R                  5       nUS:X  a7  SSKJn  U" 5       nU R                  XDR                  S5      (       a  SOS5        g US:X  aq  SSKJn  U" [        5      nUR                  S5      (       a  UR                  S	5      (       a	  [        S
S9  U R                  XDR                  S5      (       a  SOS5        g US:X  a'  [        U5      n[        5         U R                  U5        g US:X  a+  [        U5      n[        5         U R                  SS
0UE5        g US:X  a*  [        USS9n[        5         U R                  SS
0UE5        g US:X  a'  [        U5      n[        5         U R                  U5        g US:X  a'  [        U5      n[        5         U R                  U5        g US:X  a   [        U5      nU R                  S
US.5        g UR                  S5      (       aG  UR!                  S5      (       a1  UR#                  S5      S   n[%        U5      nU R                  U5        g US:X  a  [        5         U R                  S
S
S.5        g US;   a-  ['        S[)        5       ;   a  UO0 5      nU R                  U5        g U R                  SSS.S5        g ! [*         a;  nS [-        U5      ;   a  S!OSn	U R                  S[-        U5      S.U	5         S nAg S nAf[.         a(  nU R                  S[-        U5      S.S"5         S nAg S nAff = f)#Nz/api/import/graph-inbox/previewr   )preview_graph_inboxr2  r%  r  z/api/import/graph-inbox)import_graph_inboximportedTrG   z/api/ui-stater  z/api/ingestFrc  z/api/foldersr  r  r2  r   
/api/apis/z/sync/z/api/render)r2  rendered)z/api/autosave/saver  r   unknown endpointr    	duplicatei  r  )r   r  r  r  r  r  r\   r$   rH   r  r6  rN  r  r  r  r]  endswithr  rh  r  localsr!  r=   rM   )
r  r  r   r  rg  r  r   r  r  r)  s
             r   do_POSTHandler.do_POSTb
  s   		"''M	AmmoG 88B,.zz$/?/?SI 00A+B/::d##

:(>(>#$/zz$/?/?SI&27;!'}$09!t 6v 67}$)'F!t 6v 67~%*73!'}$)'2!'{" )tC 89|,,w1G1GC,!&)'}$!t >?BB-fh9NgTVW'OO53EFL 	B%Q/3SDOO53q6:DAA 	AOO53q6:C@@	Ash   AJ> .A6J> %,J> 0J> /J> 3,J>  ,J> %J> 3AJ> $J> 52J> (J> >
L31K>>L3L..L3c                 z   [        U R                  5      R                  n U R                  5       nUR                  S5      (       a\  UR	                  SS5      S   n[        X25      nU(       d  U R                  SSS.S5        g [        5         U R                  S	US
.5        g UR                  S5      (       a9  UR	                  SS5      S   nXRS'   [        U5      nU R                  S	US.5        g U R                  SSS.S5        g ! [         a(  nU R                  S[        U5      S.S5         S nAg S nAff = f)N/api/events/r  rp   Fevent not foundr  r  Tr;  r  r   r  r  r  )r   r  r  r]  rsplitrS  r  rH   r  rM   r=   )r  r  r   r   r   r  r   r  s           r   do_PATCHHandler.do_PATCH
  s   		"''	AmmoG~..;;sA.r2(;OO5;L$MsS!te <=|,,S!,R0 & )tC 89OO53EFL 	AOO53q6:C@@	As+   A"D D #AD 2D 
D:D55D:c                    [        U R                  5      R                  n UR                  S5      (       aZ  UR                  SS5      S   n[	        U5      (       d  U R                  SSS.S5        g [        5         U R                  S	US
.5        g UR                  S5      (       aI  SSKJn  U" UR                  SS5      S   5      n[        U/5      n[        5         U R                  U5        g UR                  S5      (       a<  UR                  SS5      S   n[        U/S	S9n[        5         U R                  U5        g UR                  S5      (       a  UR                  SS5      S   n[        5        nUR                  S5        UR                  SU45      n	UR                  SU45      n
UR                  5         S S S 5        [        5         U R                  S	UW
R                  W	R                  S.5        g U R                  SSS.S5        g ! , (       d  f       NY= f! [         a(  nU R                  S[!        U5      S.S5         S nAg S nAff = f)Nr  r  rp   r  Fr  r  r  T)r2  rk  z/api/folders/r   rs  z/api/graphs/)r  r  r/  r  r  )r2  rk  r  r  r  r  )r   r  r]  r  rV  r  rH   r|  rt  r  r  r*   r'   r-   rU  rM   r=   )r  r  r   rt  r   rg  r   r  r)   cur_logsr   r  s               r   	do_DELETEHandler.do_DELETE
  s   		"''6	A~..;;sA.r2'11OO5;L$MsS!t AB//0%dkk#q&9"&=>1;-@!'~..++c1-b1*G9TB!'|,,S!,R0\SKK 12"{{E	 H ++?	C JJL "  !%$'LL(0(9(9	!  OO53EFL+ "\.  	AOO53q6:C@@	AsQ   AH# 2H# AH# 0AH# 5H# 7AH?<H# <H# 
H H# #
I-IIr   )r%  )__name__
__module____qualname____firstlineno__r  r  r  r  r  r  r  __static_attributes____classcell__)r  s   @r   rv  rv  
  s5    K@ DGPAdA<9A 9Ar   rv  c                    [        U 5      n [        U 5      nU(       d  [        S5      e[        U5      nUR                  S;  a  [        S5      e[
        R                  " 5       n[        U[        U 5      [        U 5      S9n[        XQS9 n UR                  S5        [        [
        R                  " 5       U-
  S-  5      n[        [        USS	5      =(       d    UR                  5       =(       d    S	5      nS
Us=:*  =(       a    S:  Os  UUS
Us=::  a  S:  a  O  OS OSU 3SS.sS S S 5        $ ! [         a     Nf = f! , (       d  f       g = f)Nr  r  r"  r#  r    i   r$  r:  r   r%  r  r(  urllibr2  r  r  r  	transport)r  r  r!  r   r+  r   r,  r
   r  r  r   rO  rM   r   r-  r.  )	r   r!   r  r  r/  r0  r1  r2  r  s	            r   _v55b_probe_with_urllibr    s.   
C
 C$H+,,hF}}--677!G
',s#C 
	&#	HHTN t((*W4<='#x3Is{{}IJ **s*&! K5#5TU;-;P!
 
'	&  			 
'	&s1   
E D0B	E 0
D=:E <D==E  
Ec                    [        U 5      n [        U 5      nU(       d  [        S5      e[        U5      nUR                  S;  a  [        S5      e[
        R                  " 5       nSSSSSS	S
S[        [        U5      5      U/
n[        R                  " U[        [        5      SSUS-   S9n[        [
        R                  " 5       U-
  S-  5      nUR                  S:w  aW  UR                  =(       d    UR                  =(       d    SR                  5       n[!        U=(       d    SUR                   35      eUR                  =(       d    SR                  5       R#                  5       n	U	(       a&  U	S   R%                  5       (       a  [        U	S   5      OSn
SU
s=:*  =(       a    S:  Os  U
USU
s=::  a  S:  a  O  OS SS.$ SU
 3SS.$ )Nr  r  r"  curlz-Lz-sSz-oz	/dev/nullz-wz%{http_code} %{time_total}z
--max-timeTr   )r4   rN  capture_outputr!   r$  r   rY   zcurl returned r%  r  r(  r  )r  r  r!  r   r+  r   r,  r=   r   r9   r:   r>   
returncodestderrstdoutrK   RuntimeErrorr  isdigit)r   r!   r  r  r/  cmdprocr2  errr  r  s              r   _v55b_probe_with_curlr  9  s   
C
 C$H+,,hF}}--677!G 	$CLC >>I!D 4$$&0D89G!{{/dkk/R6683DN4??2C"DEE[[B%%'--/E#(U1X-=-=-?-?#eAh-QK [&&3&"1c1  :?{m7L r   c                 >   [        U 5      n [        U R                  S5      =(       d    U R                  S5      =(       d    S5      n[        U R                  S5      =(       d    U5      n[        U 5      nUU[	        U R                  S5      5      [        U R                  S5      =(       d2    U R                  S5      =(       d    U R                  S5      =(       d    S5      U[        U 5      S	S	S
S
[        5       S
S
S.nU R                  S5      (       d  S	US'   S	US'   SUS'   U$ U(       d  S	US'   S	US'   SUS'   U$ / nS
n [        XS9nU(       a  UR                  S5      US'   UR                  S5      US'   UR                  S5      US'   UR                  S5      (       a  SUS'   SUS'   S
US'   O[        UR                  S5      =(       d    S5      n
SU
s=::  a  S:  a.  O  O+SUS'   SUS'   UR                  S5      =(       d    SU
 3US'   ORSUS'   SUS'   UR                  S5      =(       d    SU
 3US'   O'SUS'   SUS'   U(       a  SR                  U5      OS US'    [        UUS   US   US   US!   US   =(       d    SUS!   US   S"9  U$ ! [         ak  nUR                  S[        U5      -   5         [        XS9n S
nAGN! [         a,  n	UR                  S[        U	5      -   5         S
n	A	 S
nAGNS
n	A	ff = fS
nAff = f! [         a     U$ f = f)#zv
V55B robust real API connectivity check.
Fixes false inactive state when urllib times out but curl succeeds locally.
r   r   r   r  r  r   r   rY   r  N)r   r   r  r  r  r  r:  r  r  r  r  r  r  r:  r  r  r  r  r    zurllib: zcurl: r  r  r  r2  r&  r   r  r  r'  r(  r!  z | zprobe failedr  r*  )r  r=   r\   r  r  r  r   r  rM   rq   r  r   r  r	  )r   r!   r  r   r  r  rM  prober  exc2status_codes              r   r3  r3  m  s   
 C
 C:#''&/:U;Fswwv(&)D$H 	*+CGGN3_swww7G_377SX>_]_`!#&#	D  779"X!*&W"X!**WFE0'= #ii6]"YY|4\!IIk2[99T??%DN%-D!" DMeii6;!<Kk'C'!+X)3%& %		' 2 Kk]6KW!+X)3%& %		' 2 Kk]6KW#X!+.4

6*.W>]+ .^,G}*n- 01		
 K_  0j3s8+,	0)#?E 	0MM(SY.//	00X  KsN   	J &.L 
L L>	K
LK>4L>LLL
LLc                    [        5         [        5        Vs/ s H  n[        U5      PM     nnU Vs/ s H  n[        X5      (       d  M  UPM     nnU Vs/ s H  n[	        USS9PM     nnU(       d  SnOP[        S U 5       5      (       a  SnO6[        S U 5       5      (       a  SnO[        S U 5       5      (       a  SnOS	nS
U U[        5       [        U5      US.$ s  snf s  snf s  snf )zL
V55B endpoint backend:
GET /api/apis/status
GET /api/apis/status?graph=ISS
r   r    r  c              3   0   #    U  H  oS    S:H  v   M     g7fr8  r   r9  s     r   r`  r:    r;  r<  r!  c              3   0   #    U  H  oS    S:H  v   M     g7fr>  r   r9  s     r   r`  r:    r;  r<  r'  c              3   0   #    U  H  oS    S:H  v   M     g7fr@  r   r9  s     r   r`  r:    rA  r<  r&  TrB  rE  rF  s         r   rJ  rJ    s	     ;C 	# 	  S( 	t   C 	sB/  
 	88	8	8	88	8	8	7w7	7	7 iW 5

s   C C%
C%C*__main__rp   i="  r  z%GRAFO D API server: http://127.0.0.1:z1/outputs/timeline_excel_like.html?v=sqlite_serverz	127.0.0.1)F)rY   )Nr   )T)r  r   r2  rY   r0   )r   )r   )r   )grY  rf  r"   r9   r;   rD   r   r   r   r   pathlibr   http.serverr   r   r|  r   r	   urllib.requestr
   r   urllib.errorr   r   grafo_d_sqlite_adapterr   __file__resolveparentr>   r$   r6   ALLOWED_UI_KEYSre  rr  Lockr8   r   r*   r.   r1   rH   rW   ra   r|   r   r   r,   r   r   r   r   r   r   r+  r6  rD  rN  rS  rV  rq  r  r  r  r  r  r  r  r  r  r  r<  r  r  r  r  r  r  r   r  r	  r  r  r  r3  r5  rJ  rQ  rS  ra  rh  rp  rt  rv  r  r  r  rt   argvr   portprintserve_foreverr   r   r   <module>r     sg       
    '  E + + , 0H~&&	J22	&	&M  ~~>2
	;48	T
(#L
"
%Rk8\qlWt.-b/d	 (XqhXv4.W(t=*@TvD4N&VD="4n
4Z3z
8$:|/h">9;x,tn")X !9 @ $Yxnh8|A& |AH#
L1h\~)\ z"388}q03sxx{dD$	1$7h
ijd+W5CCE r   