+
    j:F                        R t ^ RIt^ RIt^ RIt^ RIt^ RIHtHt ^ RIHt ^ RI	H
t
 ^ RIHtHtHt ^ RIHt ^ RIHt ^ RIt]P(                  P+                  R4      t]P.                  P1                  R	R
4      t]P.                  P1                  RR4      tR tR R lt]R R l4       t]! R]R7      t]P?                  ]R.R.R.R7       R R lt ]P1                  R4      R 4       t!]P1                  R4      R 4       t"]P1                  R4      R 4       t#]PI                  R4      R@R R  ll4       t%]P1                  R!4      RAR" R# ll4       t&]P1                  R$4      RBR% R& ll4       t']P1                  R'4      RBR( R) ll4       t(]PI                  R*4      R+ R, l4       t)R- R. lt*R/ R0 lt+R1 R2 lt,R3 R4 lt-R5 R6 lt.RCR7 R8 llt/R9 R: lt0]1R;8X  d:   ]2! ]P.                  P1                  R<R=4      4      t3]Ph                  ! ]R>]3R?7       R# R# )Du   Health Bridge — endpoint receptor de datos de Health Connect.

Recibe POST del Pixel 9a, almacena en SQLite, expone API para Pipo.
Solo escucha en Tailscale (no público).
N)datetimetimezone)asynccontextmanager)ZoneInfo)FastAPIRequestHTTPException)CORSMiddleware)FileResponsez~/health-bridge/health.dbHEALTH_BRIDGE_TOKENchangemeHEALTH_BRIDGE_TIMEZONEzAmerica/Argentina/Buenos_Airesc                      \         P                  ! \        4      p V P                  R 4       V P	                  R4       \        V 4       V P                  4        V # )zPRAGMA journal_mode=WALay  
        CREATE TABLE IF NOT EXISTS raw_data (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            received_at TEXT NOT NULL DEFAULT (datetime('now')),
            data_type TEXT NOT NULL,
            record_key TEXT,
            local_date TEXT,
            source TEXT,
            data_json TEXT NOT NULL
        );
        CREATE INDEX IF NOT EXISTS idx_raw_data_type ON raw_data(data_type);
        CREATE INDEX IF NOT EXISTS idx_raw_data_received ON raw_data(received_at);

        CREATE TABLE IF NOT EXISTS daily_summary (
            date TEXT PRIMARY KEY,
            steps INTEGER DEFAULT 0,
            distance_m REAL DEFAULT 0,
            floors REAL DEFAULT 0,
            active_minutes INTEGER DEFAULT 0,
            exercise_minutes INTEGER DEFAULT 0,
            calories REAL DEFAULT 0,
            active_kcal REAL DEFAULT 0,
            total_kcal REAL DEFAULT 0,
            heart_rate_avg REAL,
            heart_rate_min REAL,
            heart_rate_max REAL,
            sleep_minutes INTEGER DEFAULT 0,
            workouts INTEGER DEFAULT 0,
            weight_kg REAL,
            body_fat_percentage REAL,
            height_m REAL,
            protein_g REAL DEFAULT 0,
            carbs_g REAL DEFAULT 0,
            fat_g REAL DEFAULT 0,
            hydration_ml REAL DEFAULT 0,
            updated_at TEXT NOT NULL DEFAULT (datetime('now'))
        );
    )sqlite3connectDB_PATHexecuteexecutescript_migrate_schemacommit)conns    	server.pyinit_dbr      sI    ??7#DLL*+ % %	L DKKMK    c                8    V ^8  d   QhR\         P                  /# )   r   )r   
Connection)formats   "r   __annotate__r   F   s     $f $f',, $fr   c                   V P                  R4      P                  4        Uu0 uF  q^,          kK  	  ppRRRRRR/P                  4        F  w  r4W29  g   K  V P                  V4       K   	  V P                  R4      P                  4        Uu0 uF  q^,          kK  	  ppR	R
RRRRRRRRRRRRRRRRRRRRRR /P                  4        F  w  r4W59  g   K  V P                  V4       K   	  V P                  R!4      P                  4       pV'       d8   R"V^ ,          ;'       g    R#P	                  4       9   d   V P                  R$4       V P                  R%4       V P                  R&4       R'# u upi u upi )(zGKeep older field deployments compatible without losing stored raw data.zPRAGMA table_info(raw_data)
record_keyz/ALTER TABLE raw_data ADD COLUMN record_key TEXT
local_datez/ALTER TABLE raw_data ADD COLUMN local_date TEXTsourcez+ALTER TABLE raw_data ADD COLUMN source TEXTz PRAGMA table_info(daily_summary)
distance_mz>ALTER TABLE daily_summary ADD COLUMN distance_m REAL DEFAULT 0floorsz:ALTER TABLE daily_summary ADD COLUMN floors REAL DEFAULT 0exercise_minuteszGALTER TABLE daily_summary ADD COLUMN exercise_minutes INTEGER DEFAULT 0active_kcalz?ALTER TABLE daily_summary ADD COLUMN active_kcal REAL DEFAULT 0
total_kcalz>ALTER TABLE daily_summary ADD COLUMN total_kcal REAL DEFAULT 0workoutsz?ALTER TABLE daily_summary ADD COLUMN workouts INTEGER DEFAULT 0body_fat_percentagez=ALTER TABLE daily_summary ADD COLUMN body_fat_percentage REALheight_mz2ALTER TABLE daily_summary ADD COLUMN height_m REAL	protein_gz=ALTER TABLE daily_summary ADD COLUMN protein_g REAL DEFAULT 0carbs_gz;ALTER TABLE daily_summary ADD COLUMN carbs_g REAL DEFAULT 0fat_gz9ALTER TABLE daily_summary ADD COLUMN fat_g REAL DEFAULT 0hydration_mlz@ALTER TABLE daily_summary ADD COLUMN hydration_ml REAL DEFAULT 0zWSELECT sql FROM sqlite_master WHERE type = 'index' AND name = 'idx_raw_data_record_key'WHERE z"DROP INDEX idx_raw_data_record_keyzJCREATE INDEX IF NOT EXISTS idx_raw_data_local_date ON raw_data(local_date)zQCREATE UNIQUE INDEX IF NOT EXISTS idx_raw_data_record_key ON raw_data(record_key)N)r   fetchallitemsfetchoneupper)r   rraw_colscolddlsummary_cols	index_sqls   &      r   r   r   F   s   "ll+HIRRTUT!THUGG? eg	
 LL #',,/Q"R"["["]^"]QaDD"]L^VNeXVU^HTPLZ eg "LL" ahj  W1!3!3 : : <<9:LL]^LLdeE V _s   E-E2c                $    V ^8  d   QhR\         /# )r   app)r   )r   s   "r   r   r   m   s       r   c                   "   \        4       V P                  n        R 5x  V P                  P                  P                  4        R # 5iN)r   statedbclose)r<   s   &r   lifespanrB   l   s)     9CIIL	IILLs   AAzHealth Bridge)titlerB   *)allow_originsallow_methodsallow_headersc                $    V ^8  d   QhR\         /# r   requestr   )r   s   "r   r   r   w   s      w r   c                    "   V P                   P                  R R4      P                  RR4      pV\        8w  d   \	        RRR7      hV# 5i)Authorizationr0   zBearer i  zInvalid tokenstatus_codedetail)headersgetreplace
AUTH_TOKENr   )rJ   tokens   & r   check_tokenrV   w   sB     OO4<<YKE
ODDLs   AA	z/healthc                  v   "   R RR\         P                  ! \        P                  4      P	                  4       /# 5i)statusoktime)r   nowr   utc	isoformat r   r   healthr_      s*     dFHLL$>$H$H$JKKs   79/c                     "   R RRR/# 5i)rX   rY   servicezhealth-bridger^   r^   r   r   rootrc      s     dI77s   	z	/downloadc                     "   \         P                  P                  R 4      p \         P                  P                  V 4      '       g   \	        RRR7      h\        V RRR7      # 5i)z(~/health-bridge/static/health-bridge.apki  zAPK not found. Build it first.rN   z'application/vnd.android.package-archivezhealth-bridge.apk)
media_typefilename)ospath
expanduserexistsr   r
   )apk_paths    r   download_apkrl      sL     ww!!"LMH77>>(##4TUU-Vatuus   A A"z/ingestc                0    V ^8  d   QhR\         R\        /# )r   rJ   _token)r   str)r   s   "r   r   r      s     ,W ,W' ,W3 ,Wr   c                  "   \        V 4      G Rj  xL
  V P                  4       G Rj  xL
 pVP                  R. 4      pVP                  R4      ;'       g    \        pV'       g   \	        RRR7      h\
        P                  P                  p^ p\        4       pV F  p\        W4      p	VP                  RV	R,          V	R	,          V	R
,          V	R,          \        P                  ! V	R,          RRR7      34       V	R
,          '       d   VP                  V	R
,          4       V^,          pK  	  VP                  4        \        V4       F  p
\        WZ4       K  	  RRRVR\        V4      /#  EL\ ELG5i)z+Recibe un batch de datos de Health Connect.Nrecordsr   i  zNo records providedrN   a  
            INSERT INTO raw_data (data_type, record_key, local_date, source, data_json, received_at)
            VALUES (?, ?, ?, ?, ?, datetime('now'))
            ON CONFLICT(record_key) DO UPDATE SET
                data_type = excluded.data_type,
                local_date = excluded.local_date,
                source = excluded.source,
                data_json = excluded.data_json,
                received_at = datetime('now')
            	data_typer    r!   r"   recordT)
separators	sort_keysrX   rY   dates_rebuilt,:)rV   jsonrR   DEFAULT_TIMEZONEr   r<   r?   r@   set_normalize_recordr   dumpsaddr   sorted_rebuild_daily_summary)rJ   rn   bodyrq   	client_tzr@   countaffected_datesrs   
normalizedr!   s   &&         r   ingestr      sD     g
Dhhy"%G$88(8I4IJJ YY\\BEUN&v9



	 ;'<(<(8$

:h/JRVW	
& l##z,78
/ 2 IIK^,
r. - dIuovn?UVVU s,   E3E-E3E0-E3E3'DE30E3z/datac                <    V ^8  d   QhR\         R\        R\        /# )r   rJ   rr   limit)r   ro   int)r   s   "r   r   r      s!      G  3 r   c                  "   \        V 4      G Rj  xL
  \        P                  P                  pV'       d#   VP	                  RW34      P                  4       pM!VP	                  RV34      P                  4       pR\        V4      RV Uu. uF+  pR\        P                  ! V^ ,          4      RV^,          /NK-  	  up/#  Lu upi 5i)zConsulta datos almacenados.NzXSELECT data_json, received_at FROM raw_data WHERE data_type = ? ORDER BY id DESC LIMIT ?zDSELECT data_json, received_at FROM raw_data ORDER BY id DESC LIMIT ?r   datars   received_at)	rV   r<   r?   r@   r   r1   lenrz   loads)rJ   rr   r   r@   rowsr5   s   &&&   r   get_datar      s      g
 YY\\Bzzf
 (* 	
 zzRH
 (* 	 	TDQDq(DJJqt,mQqTBDQ    Rs'   CC %CAC1C
<CCz/summaryc                0    V ^8  d   QhR\         R\        /# r   rJ   daysr   r   )r   s   "r   r   r      s     
8 
8w 
8c 
8r   c           
     j  "   \        V 4      G Rj  xL
  \        P                  P                  pVP	                  RV34      P                  4       pVP	                  R4      P                   Uu. uF  qD^ ,          NK  	  ppRV Uu. uF  p\        \        WV4      4      NK  	  up/#  Lu upi u upi 5i)u'   Resumen diario de los últimos N días.N6SELECT * FROM daily_summary ORDER BY date DESC LIMIT ?#SELECT * FROM daily_summary LIMIT 0r   )	rV   r<   r?   r@   r   r1   descriptiondictzip)rJ   r   r@   r   desccolsr5   s   &&     r   get_summaryr      s      g
 YY\\B::@	 hj 	
 !#

+P Q ] ]^ ]GG ]D^6AT#d,'677  _6s.   B3B'AB3.B)?B3B.#B3)
B3z	/overviewc                0    V ^8  d   QhR\         R\        /# r   r   )r   s   "r   r   r      s       s r   c           	       "   \        V 4      G Rj  xL
  \        P                  P                  pVP	                  RV34      P                  4       pVP	                  R4      P                   Uu. uF  qD^ ,          NK  	  ppV Uu. uF  p\        \        WV4      4      NK  	  ppV'       d
   V^ ,          MRpR\        RVRV/#  Lu upi u upi 5i)z.Health overview optimized for Pipo dashboards.Nr   r   r   
latest_dayr   )
rV   r<   r?   r@   r   r1   r   r   r   r{   )	rJ   r   r@   r   r   r   r5   	summarieslatests	   &&       r   get_overviewr      s      g
 YY\\B::@	 hj 	 !#

+P Q ] ]^ ]GG ]D^-12Tc$l#TI2&Yq\DF$f	   _2s.   CCAC.C?CC
""C
Cz/rebuild-summaryc                $    V ^8  d   QhR\         /# rI   rK   )r   s   "r   r   r      s     4 47 4r   c                  "   \        V 4      G Rj  xL
  \        P                  P                  pVP	                  R4      P                  4        Uu. uF  q"^ ,          NK  	  ppV F  p\        W4       K  	  RRRV/#  Llu upi 5i)zGRebuild all daily summaries from raw_data. Useful after schema changes.NzYSELECT DISTINCT local_date FROM raw_data WHERE local_date IS NOT NULL ORDER BY local_daterX   rY   rv   )rV   r<   r?   r@   r   r1   r   )rJ   r@   r5   datesr!   s   &    r   rebuild_summaryr      s      g
 YY\\Bjjg

(*!  
 
 
r. dOU33 s!   BA>?BB "B Bc                <    V ^8  d   QhR\         R\        R\         /# )r   rs   r   returnr   ro   )r   s   "r   r   r      s!      d s t r   c           
         V P                  R R4      pV P                  R4      ;'       g    V P                  R4      pV'       g   \        W4      p\        V 4      pV'       d   W4R&   VP                  R4      ;'       g5    VP                  R4      ;'       g    VP                  R4      ;'       g    RpVP                  R4      ;'       g    VP                  R	4      pV'       g   \        W$V4      pWdR&   WTR&   R
VRVRVRVRV/# )typeunknownr!   dater"   r<   package_namehealth_connectr    idrr   rs   )rR   _local_date_for_recordr   _stable_record_key)rs   r   rr   r!   r   r"   r    s   &&     r   r}   r}      s    

69-IL)??VZZ-?J+F>
fJ#-< ^^H%tt)>tt*..Q_B`ttdtF-EE1EJ'	zJ
)|!x 	Yjj&* r   c                J    V ^8  d   QhR\         R\        R\        R,          /# )r   rs   r   r   Nr   )r   s   "r   r   r     s%     	 	4 	C 	C$J 	r   c                     \        V4      pR FU  pV P                  V4      pV'       g   K   \        V4      P                  V4      P	                  4       P                  4       u # 	  R#   \         d     Ki  i ; i)rZ   N)rZ   startperiod_start)_zonerR   _parse_instant
astimezoner   r]   
ValueError)rs   r   zonefieldvalues   &&   r   r   r     so    D2

5!5%e,77=BBDNNPP	 3   s   5A))A87A8c                V    V ^8  d   QhR\         R\        R\         R,          R\         /# )r   rr   rs   r!   Nr   )ro   r   )r   s   "r   r   r   $  s-     # ## #t #t #PS #r   c                 d   V R9   d   V'       d   V  RV 2# RV RVRVP                  R4      RVP                  R4      RVP                  R4      RVP                  R4      RVP                  R4      R	VP                  R	4      /p\        ;QJ d*    R
 VP                  4        4       F  '       g   K   RM	  RM! R
 VP                  4        4       4      '       g   Tp\        P                  ! \
        P                  ! VRRR7      P                  4       4      P                  4       R,          pV  RV 2# )daily_aggregatery   r   r!   rZ   r   endr   
period_endr"   c              3   (   "   T F  qR Jx  K
  	  R # 5ir>   r^   ).0vs   & r   	<genexpr>%_stable_record_key.<locals>.<genexpr>1  s     8&7}&7s   TF)ru   rt   :N   N   
aggregatedr   rw   )	rR   anyvalueshashlibsha256rz   r~   encode	hexdigest)rr   rs   r!   identitydigests   &&&  r   r   r   $  s    55*Aj\**	j

6"G$vzz% 

>2fjj.&**X&	H 38hoo&783338hoo&7888^^DJJx4JW^^`akkmnqrF[&""r   c                0    V ^8  d   QhR\         R\        /# )r   namer   )ro   r   )r   s   "r   r   r   6  s     * * * *r   c                 Z     \        V 4      #   \         d    \        \        4      u # i ; ir>   )r   	Exceptionr{   )r   s   &r   r   r   6  s,    *~ *())*s   
 **c                0    V ^8  d   QhR\         R\        /# )r   r   r   )ro   r   )r   s   "r   r   r   <  s     @ @# @( @r   c                 N    \         P                  ! V P                  R R4      4      # )Zz+00:00)r   fromisoformatrS   )r   s   &r   r   r   <  s    !!%--X">??r   c                `    V ^8  d   QhR\         \        ,          R\        R\        R,          /# )r   rq   r   r   N)listr   ro   )r   s   "r   r   r   ?  s)     7 7T$Z 7 7 7r   c                    a V  Uu. uF  q"P                  S4      '       g   K  VNK  	  ppV'       g   V '       d
   V R,          # R# \        VV3R lR7      # u upi )   Nc                 &   < V P                  S4      # r>   )rR   )r5   r   s   &r   <lambda>_latest.<locals>.<lambda>C  s    ur   )key)rR   max)rq   r   r5   
candidatess   &f  r   _latestr   ?  sH    $5We!!WJ5%wr{/4/z566 6s
   AAc                D    V ^8  d   QhR\         P                  R\        /# )r   r@   r!   )r   r   ro   )r   s   "r   r   r   E  s$     z zw11 zs zr   c                n   V P                  RV34      P                  4       pV Uu. uF   p\        P                  ! V^ ,          4      NK"  	  pp^ pRpRp^ p^ p	Rp
RpRp^ p^ p. p. p. p. pRpRpRpRpV EF  pVP	                  RR4      pVR$9   d   \        VP	                  RV4      ;'       g    ^ 4      p\        VP	                  RV4      ;'       g    ^ 4      p\        VP	                  RV4      ;'       g    ^ 4      p\        VP	                  RV4      ;'       g    ^ 4      p\        VP	                  R	V4      ;'       g    ^ 4      pT;'       g    Tp
K  VR8X  d0   T\        VP	                  R
^ 4      ;'       g    ^ 4      ,          pEK  VR8X  d0   T\        VP	                  R^ 4      ;'       g    ^ 4      ,          pEKP  VR8X  d0   T
\        VP	                  R^ 4      ;'       g    ^ 4      ,          p
EK  VR8X  d   VP	                  R4      pV'       d   VP                  \        V4      4       VP	                  R4      pV'       d?   V F8  pVP	                  R4      pV'       g   K  VP                  \        V4      4       K:  	  V'       g6   VP	                  R4      pVe   VP                  \        V4      4       EKQ  EKT  EKW  VR8X  d0   T\        VP	                  R^ 4      ;'       g    ^ 4      ,          pEK  VR8X  d9   T	\        VP	                  R^ 4      ;'       g    ^ 4      ,          p	V^,          pEK  VR8X  d   VP                  V4       EK  VR8X  d   VP                  V4       EK  VR8X  d   VP                  V4       EK  VR8X  d   T\        VP	                  R^ 4      ;'       g    ^ 4      ,          pT\        VP	                  R^ 4      ;'       g    ^ 4      ,          pT\        VP	                  R^ 4      ;'       g    ^ 4      ,          pT
\        VP	                  R^ 4      ;'       g    ^ 4      ,          p
EK  VR8X  g   EK  T\        VP	                  R^ 4      ;'       g    ^ 4      ,          pEK  	  V'       d   \        V4      \        V4      ,          MRpV'       d   \        V4      MRpV'       d   \        V4      MRp \        V4      p!\        V4      p"\        V4      p#T P                  R YYgYYTTTT TTV!'       d&   V!P	                  R!4      e   \        V!R!,          4      MRV"'       d&   V"P	                  R"4      e   \        V"R",          4      MRV#'       d&   V#P	                  R#4      e   \        V#R#,          4      MRVVVV34       V P                  4        R# u upi )%zNRebuild one local-day summary from raw_data. Never increments previous totals.zTSELECT data_json FROM raw_data WHERE local_date = ? ORDER BY received_at ASC, id ASCg        r   r0   stepsr#   r$   r&   r'   r   active_minutesminutescalorieskcal
heart_ratebpmsamplesavg_bpmNsleepexerciseweightbody_fatheight	nutritionr+   r,   r-   energy_kcal	hydration	volume_mla  
        INSERT INTO daily_summary (
            date, steps, distance_m, floors, active_minutes, exercise_minutes,
            calories, active_kcal, total_kcal, heart_rate_avg, heart_rate_min,
            heart_rate_max, sleep_minutes, workouts, weight_kg,
            body_fat_percentage, height_m, protein_g, carbs_g, fat_g, hydration_ml,
            updated_at
        )
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
        ON CONFLICT(date) DO UPDATE SET
            steps = excluded.steps,
            distance_m = excluded.distance_m,
            floors = excluded.floors,
            active_minutes = excluded.active_minutes,
            exercise_minutes = excluded.exercise_minutes,
            calories = excluded.calories,
            active_kcal = excluded.active_kcal,
            total_kcal = excluded.total_kcal,
            heart_rate_avg = excluded.heart_rate_avg,
            heart_rate_min = excluded.heart_rate_min,
            heart_rate_max = excluded.heart_rate_max,
            sleep_minutes = excluded.sleep_minutes,
            workouts = excluded.workouts,
            weight_kg = excluded.weight_kg,
            body_fat_percentage = excluded.body_fat_percentage,
            height_m = excluded.height_m,
            protein_g = excluded.protein_g,
            carbs_g = excluded.carbs_g,
            fat_g = excluded.fat_g,
            hydration_ml = excluded.hydration_ml,
            updated_at = datetime('now')
    kg
percentagemetersr   )r   r1   rz   r   rR   r   floatappendsumr   minr   r   r   )$r@   r!   r   rowrq   r   r#   r$   r   r%   r   r&   r'   sleep_minutesr(   	hr_valuesweights	body_fatsheightsr+   r,   r-   r.   r5   thrr   sr   r   hr_avghr_minhr_maxlatest_weightlatest_body_fatlatest_heights$   &&                                  r   r   r   E  s   ::^	 hj 	 .22Tctzz#a&!TG2EJFNHKJMHIGIGIGELEE&"11gu-223Equu\:>CC!DJ1556277a8Fm[ A F FQGKquu\:>CC!DJ!00[H'\Sw*//a00E""c!%%	1"5":":;;N*_aeeFA.33!44H,uB  r+eeI&G A%%,Cs!((s4 ! %%	*&$$U7^4 '  '\Sy!!4!9!9::M*_AEE)Q$7$<$<1 ==MH(]NN1*_Q(]NN1+quu[!499::IuQUU9a055A66GU155!,1122EaeeM15::;;H+E!%%Q"7"<"<1==L] ` 1:S^c)n,tF(S^dF(S^dFG$Mi(OG$MJJ @ 	:~z666=&38I8I$8O8[mD!"ae0?ODWDWXdDeDqol+,w{*7M<M<Mh<W<cmH%&im7E<
?'P IIKi 3s   &V2__main__PORT3007z0.0.0.0)hostportr>   )Nd   )   )rZ   )5__doc__rz   r   rg   r   r   r   
contextlibr   zoneinfor   fastapir   r   r   fastapi.middleware.corsr	   fastapi.responsesr
   uvicornrh   ri   r   environrR   rT   r{   r   r   rB   r<   add_middlewarerV   r_   rc   rl   postr   r   r   r   r   r}   r   r   r   r   r   r   __name__r   r  runr^   r   r   <module>r&     s  
   	  ' *  3 3 2 * 
''

8
9ZZ^^1:>
::>>":<\] +Z$fL  
 Oh7   >#se\_[`  a L L 8 8 v v ),W ,W\  * 
8 
8  " 
4 42	#$*@7zx zrzz~~ff-.DKK)$/ r   