o
    )å²i×Ø  ã                   @   sd  d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	m	Z	m
Z
mZ ddlmZ e d¡Ze deeeƒ ¡ jjd ƒ¡Zeeeƒ ¡ jjd d	 ƒZe d
d¡Ze dd¡Ze dd¡Ze dd¡ZdZdZdZdZdZdZ dZ!dZ"e dee #¡ d d ƒ¡Z$ee #¡ d ƒee #¡ d d d d ƒeeeƒjd ƒgZ%G dd„ dƒZ&dS )u  
Dashboard Scheduler â€” automated data refreshes, proactive WhatsApp nudges,
weekly reports, health data ingestion (Oura API + Health Connect fallback),
Gmail briefing integration, and 'on this day' insights.

Runs as a background thread inside the Flask app.

Health data pipeline:
  PRIMARY: Oura API v2 â†’ sleep, steps, exercise, HRV, readiness (direct API pull)
  FALLBACK: Google Drive API â†’ Health Connect zip â†’ weight, body fat, blood pressure
  FALLBACK2: Filesystem watcher on local Google Drive sync + ~/clawd/health-imports/
é    N)ÚdatetimeÚdateÚ	timedelta)ÚPathÚ	schedulerÚDASHBOARD_DATA_PATHzdashboard-data.jsonÚmemoryzchatgpt-kb.dbÚTWILIO_ACCOUNT_SIDÚ ÚTWILIO_AUTH_TOKENÚTWILIO_PHONE_NUMBERÚBILL_PHONE_NUMBERé   é   é   é   é	   é  i,  ÚHEALTH_CONNECT_DIRzGoogle DrivezMy DriveÚLibraryÚCloudStoragezGoogleDrive-syrros@gmail.comzhealth-importsc                   @   s  e Zd ZdZdd„ Zdd„ Zdd„ Zdd	„ Zd
d„ Zdd„ Z	dd„ Z
dd„ Zdd„ Zdd„ Zdd„ Zdd„ Zdd„ Zdd„ Zdd„ ZeZd d!„ Zd"d#„ Zd$d%„ Zd&d'„ Zd(d)„ Zd*d+„ Zd,d-„ Zd.d/„ Zd0d1„ Zd2d3„ Zd4d5„ Zd6d7„ Zd8d9„ Z d:d;„ Z!d<d=„ Z"d>d?„ Z#d@dA„ Z$dBS )CÚDashboardSchedulerzCBackground scheduler that runs data refreshes and proactive nudges.c                 C   sl   || _ d| _d | _d| _d | _d | _d | _d | _d| _t	ƒ | _
d | _d | _d | _i | _d | _d | _d| _d S )NFr   )ÚdmÚrunningÚthreadÚ_last_refreshÚ_last_morningÚ_last_eveningÚ_last_weeklyÚ_last_briefing_updateÚ_last_health_checkÚsetÚ_ingested_filesÚ_gdrive_connectorÚ_gmail_briefingÚ_gdrive_last_file_idÚ_last_email_nudgesÚ_last_new_musicÚ_last_health_dailyÚ_last_pregame_rapid)ÚselfÚdashboard_manager© r,   úD/sessions/festive-kind-sagan/mnt/clawd/whatsapp-agent/./scheduler.pyÚ__init__B   s"   
zDashboardScheduler.__init__c                 C   s:   | j rdS d| _ tj| jdd| _| j ¡  t d¡ dS )z+Start the scheduler in a background thread.NT)ÚtargetÚdaemonzScheduler started)r   Ú	threadingÚThreadÚ	_run_loopr   ÚstartÚloggerÚinfo©r*   r,   r,   r-   r4   U   s   
zDashboardScheduler.startc                 C   s   d| _ t d¡ dS )zStop the scheduler.FzScheduler stoppedN)r   r5   r6   r7   r,   r,   r-   Ústop^   s   zDashboardScheduler.stopc              
   C   sä  t  d¡ |  ¡  | jrpz:t ¡ }t   ¡ | j tkr%|  ¡  t   ¡ | _|j	t
krF|jtkrF|jtd k rF| j| ¡ krF|  ¡  | ¡ | _|j	tkrg|jtkrg|jtd k rg| j| ¡ krg|  ¡  | ¡ | _| ¡ tkr‡|j	tkr‡|jdk r‡| j| ¡ kr‡|  ¡  | ¡ | _d|j	  kr’dkr¦n nt   ¡ | j tkr¦|  ¡  t   ¡ | _|j	dkrÀ|jdk rÀ| j| ¡ krÀ|  ¡  | ¡ | _g d¢}|j	|v rç|jdk rç| j |j	¡| ¡ krç|   |j	¡ | ¡ | j|j	< | ¡ dkr|j	dkr|jdk r| j!| ¡ kr|  "¡  | ¡ | _!| j#|j	kr|  ¡  |j	| _#t   ¡ | j$ d	krGz|  %|¡ W n t&yF } zt' (d
|› ¡ W Y d}~nd}~ww W n t&ye } zt'j)d|› dd W Y d}~nd}~ww t  d¡ | jsdS dS )u0   Main scheduler loop â€” checks every 30 seconds.é
   é   r   r   é   ©r   é   é   é   r   i„  z*Rapid pregame check error (non-critical): NzScheduler loop error: T©Úexc_infor   )*ÚtimeÚsleepÚ_update_briefing_datar   r   Únowr   ÚDATA_REFRESH_INTERVALÚ_refresh_dataÚhourÚMORNING_NUDGE_HOURÚminuteÚMORNING_NUDGE_MINr   r   Ú_send_morning_nudgeÚEVENING_NUDGE_HOURÚEVENING_NUDGE_MINr   Ú_send_evening_nudgeÚweekdayÚWEEKLY_REPORT_DOWÚWEEKLY_REPORT_HOURr   Ú_send_weekly_reportr    ÚHEALTH_CHECK_INTERVALÚ_check_health_connectr(   Ú_daily_health_grabr&   ÚgetÚ_send_email_digestr'   Ú_send_new_musicr   r)   Ú_maybe_rapid_pregame_refreshÚ	Exceptionr5   ÚdebugÚerror)r*   rE   ÚEMAIL_NUDGE_HOURSÚer,   r,   r-   r3   c   s€   

















€ÿ€ €ÿ
¯zDashboardScheduler._run_loopc           
   
   C   sÌ  zÈt  d¡ | j ¡ }d}|  ¡ }|r5d|vri |d< ||d d< t ¡  ¡ |d d< d}t  d|› ¡ t 	¡  ¡ }| 
d¡|krH||d< d}|  |¡}|rU||d	< d}z"|  ¡ }|d
urv|t ¡  ¡ dœ|d< d}t  dt|ƒ› d¡ W n ty‘ } zt  d|› ¡ W Y d
}~nd
}~ww |rÆddlm} |t| jjƒd dd}	|	 | j |¡ W d
  ƒ n1 s¹w   Y  t  d¡ W d
S W d
S  tyå } zt jd|› dd W Y d
}~d
S d
}~ww )z?Refresh live data in dashboard-data.json (crypto prices, etc.).z!Running scheduled data refresh...FÚcryptoÚ	cro_priceÚcro_updatedTzCRO price refreshed: $r   Ú	nextEventN©ÚgamesÚupdatedÚbettingzBetting picks refreshed: z gamesz-Betting picks refresh failed (non-critical): r   )ÚFileLockz.lockr9   )ÚtimeoutzDashboard data refreshedzData refresh failed: r@   )r5   r6   r   ÚreadÚ_fetch_cro_pricer   rE   Ú	isoformatr   ÚtodayrW   Ú_get_next_eventÚ_fetch_betting_picksÚlenr[   r\   Úfilelockrh   ÚstrÚ	file_pathÚ_writer]   )
r*   ÚdataÚchangedra   Ú	today_strÚupcomingÚpicksr_   rh   Úlockr,   r,   r-   rG   ¿   sZ   




þ€€ÿÿú"€ÿz DashboardScheduler._refresh_datac              
   C   sv   zt jddddœdd}|jdkr| ¡  di ¡ d¡W S W d	S  ty: } zt d|› ¡ W Y d	}~d	S d	}~ww )
zFetch CRO price from CoinGecko.z-https://api.coingecko.com/api/v3/simple/pricezcrypto-com-chainÚusd)ÚidsÚvs_currenciesr9   )Úparamsri   éÈ   zCRO price fetch failed: N)ÚrequestsrW   Ústatus_codeÚjsonr[   r5   Úwarning)r*   Úrespr_   r,   r,   r-   rk   ô   s   ý
ÿþ€þz#DashboardScheduler._fetch_cro_pricec              	   C   sj   t  ¡ }| ¡ }| dg ¡D ]$}| dd¡|kr2|d |d | dd¡| dd¡| dd¡dœ  S qd	S )
zFind the next upcoming event.Úscheduler   r
   ÚtitlerB   ÚlocationÚnote)r†   r   rB   r‡   rˆ   N)r   rm   rl   rW   )r*   ru   rm   Únow_strÚeventr,   r,   r-   rn     s   



ûÿz"DashboardScheduler._get_next_eventc              
   C   ór   | j du r/zddlm} |ƒ | _ W n ty. } zt d|› ¡ d| _ W Y d}~nd}~ww | j dur7| j S dS )z%Lazy-load the Google Drive connector.Nr   )Úget_connectorz&Google Drive connector not available: F)r#   Úgdrive_healthrŒ   r[   r5   r\   )r*   rŒ   r_   r,   r,   r-   Ú_get_gdrive_connector  ó   
€þz(DashboardScheduler._get_gdrive_connectorc              
   C   r‹   )z$Lazy-load the Gmail briefing module.Nr   )Úget_gmail_briefingzGmail briefing not available: F)r$   Úgmail_briefingr   r[   r5   r\   )r*   r   r_   r,   r,   r-   Ú_get_gmail_briefing  r   z&DashboardScheduler._get_gmail_briefingc              
   C   sl   z|   ¡ }|r|jr|  |¡ W dS |  ¡  W dS  ty5 } ztjd|› dd W Y d}~dS d}~ww )uÙ   
        Watch for new Health Connect zip files.

        Strategy:
          1. PRIMARY: Google Drive API â€” search for files, download, parse
          2. FALLBACK: Filesystem watcher on local sync folders
        NzHealth Connect check failed: Tr@   )rŽ   Ú	availableÚ_check_health_connect_gdriveÚ _check_health_connect_filesystemr[   r5   r]   )r*   Úgdriver_   r,   r,   r-   rU   )  s   

"€ÿz(DashboardScheduler._check_health_connectc              
   C   s6  zy|j dd}|sW dS |d }|d }|| jkrW dS |d dk r5t d|d	 › d
|d › d¡ W dS t d|d	 › ¡ | ||d	 ¡}|sUt d¡ |  ¡  W dS |  |¡}|rq|| _| j	 
|¡ |  d|› ¡ |  ¡  |jdd W dS  tyš } ztjd|› dd |  ¡  W Y d}~dS d}~ww )z8Check Google Drive for new Health Connect files via API.é   ©Úmax_age_hoursNr   ÚidÚsizeé  zSkipping small Drive file: Únameú (ú bytes)z"New Health Connect file in Drive: u.   Download failed â€” falling back to filesystemu5   Luke here â€” health data auto-imported from Drive!

r   ©Úmax_age_daysz"Google Drive health check failed: Tr@   )Úsearch_health_connect_filesr%   r5   rƒ   r6   Údownload_filer]   r•   Ú_ingest_health_filer"   ÚaddÚ_send_whatsapprD   Úcleanup_old_downloadsr[   )r*   r–   ÚfilesÚlatestÚfile_idÚ
local_pathÚresultr_   r,   r,   r-   r”   >  s:   
 

€ýz/DashboardScheduler._check_health_connect_gdrivec           	      C   sô   t gt }|D ]p}tj |¡sqtj |d¡tj |d¡tj |dd¡g}|D ]O}tj|ddD ]E}|| jv r8q0tj |¡}t	 	¡ | d }|dkrKq0tj 
|¡dk rTq0t d	|› ¡ |  |¡}|ru| j |¡ |  d
|› ¡ |  ¡  q0q'qdS )z9Fallback: watch local filesystem for Health Connect zips.úHealth Connect*.zipúhealth_connect*.zipú**T©Ú	recursiver   r—   rœ   z/New Health Connect file detected (filesystem): u*   Luke here â€” health data auto-imported!

N)r   ÚHEALTH_CONNECT_ALT_DIRSÚosÚpathÚisdirÚjoinÚglobr"   ÚgetmtimerB   Úgetsizer5   r6   r¤   r¥   r¦   rD   )	r*   Úsearch_dirsÚ
search_dirÚpatternsÚpatternÚzip_pathÚmtimeÚ	age_hoursr¬   r,   r,   r-   r•   j  s8   
ý

€ïÿöz3DashboardScheduler._check_health_connect_filesystemc              
   C   sV   zddl m} ||| jƒW S  ty* } ztjd|› dd W Y d}~dS d}~ww )z!Ingest a Health Connect zip file.r   )Úingest_health_connectz!Health Connect ingestion failed: Tr@   N)Úhealth_connectrÁ   r   r[   r5   r]   )r*   r¾   rÁ   r_   r,   r,   r-   r¤   Œ  s   €þz&DashboardScheduler._ingest_health_filec              
   C   sü   t  d¡ g }z(ddlm} || jƒ}|r&d| ¡ vr&| |¡ t  d¡ nt  d|› ¡ W n tyK } zt jd|› dd	 W Y d
}~nd
}~ww |  	¡ }|rW| |¡ |rr|  
¡  d |¡}|  d|› d¡ t  d¡ d
S t  d¡ |  d¡ d
S )uX  
        Daily 8 AM health data pull.

        Pipeline:
          1. Oura API â†’ sleep, steps, exercise, HRV, readiness
          2. Health Connect (Drive) â†’ weight, body fat, blood pressure
          Each part is independent â€” Oura failure doesn't block HC and vice versa.
          Sends a WhatsApp summary of what was updated.
        z)=== Daily health grab starting (8 AM) ===r   ©Úingest_oura_dataúnot configuredu#   Daily health grab â€” Oura: SUCCESSu   Daily health grab â€” Oura: u#   Daily health grab â€” Oura failed: Tr@   Nz

u)   Good morning! ðŸ“Š Health data updated.

u   

Dashboard updated. â€” LukezDaily health grab: SUCCESSz"Daily health grab: no data updatedu±   Morning check-in â€” no new health data today. Oura data will pull automatically if configured. For weight, upload a Health Connect export to Drive or text me 'health'. â€” Luke)r5   r6   Úoura_apirÄ   r   ÚlowerÚappendr[   rƒ   Ú_daily_health_connect_pullrD   r¶   r¦   )r*   Ú	summariesrÄ   Úoura_resultr_   Ú	hc_resultÚcombinedr,   r,   r-   rV   —  s<   



€ €ÿ

ÿÿ
ÿz%DashboardScheduler._daily_health_grabc           	      C   sº  |   ¡ }|r©|jr©tddƒD ]š}zq|jdd}|rw|d }|d }|| jkr4t d|d › ¡ W  d	S |d
 dk rLt d|d › d|d
 › d¡ W q| ||d ¡}|rv|  	|¡}|rv|| _| j
 |¡ |jdd t d¡ |W   S n	t d|› d¡ W n tyž } zt d|› d|› ¡ W Y d	}~nd	}~ww |dk r¨t d¡ qz|  ¡ }|r¼d|vr¿t d¡ |W S W d	S W d	S  tyÜ } zt d|› ¡ W Y d	}~d	S d	}~ww )z«
        Pull weight/body fat from Health Connect via Google Drive.
        Retries up to 3 times with 2-minute intervals.
        Returns summary string or None.
        é   r?   r—   r˜   r   rš   z"Already ingested today's HC file: r   Nr›   rœ   zSkipping small file: rž   rŸ   r   r    zDaily HC pull: SUCCESS (Drive)zDaily HC pull attempt z/3: no files found yetz/3 failed: é   éx   zNo Health Connectz#Daily HC pull: SUCCESS (filesystem)z*Daily HC pull filesystem fallback failed: )rŽ   r“   Úranger¢   r%   r5   r6   rƒ   r£   r¤   r"   r¥   r§   r[   rB   rC   Útrigger_health_ingest)	r*   r–   Úattemptr¨   r©   rª   r«   r¬   r_   r,   r,   r-   rÉ   È  s^   

 


€€"€ÿ
€
þúý€ýz-DashboardScheduler._daily_health_connect_pullc              
   C   sì  zØ|   ¡ }|r
|jsW dS | ¡ }| d¡sW dS ddddœ}| |d¡}|› dg}| d¡ | d	¡}|rG| d
|d › d|d › d¡ | dg ¡}|rq| d¡ | d¡ |D ]}	| d|	d › d|	d dd… › ¡ q[| dg ¡}
|
r¡|s¡| d¡ | d¡ |
dd… D ]}	| d|	d › d|	d dd… › ¡ q‹| dg ¡}|r³| dt|ƒ› ¡ |rÎ|d dkrÎ|  d |¡¡ t 	|› d¡ W dS t 
|› d¡ W dS  tyõ }	 ztjd|	› d d! W Y d}	~	dS d}	~	ww )"z4Send a Gmail digest via WhatsApp at scheduled times.Nr“   ÚMorningÚMiddayÚEveningr<   r
   u    Inbox Update â€” LukeÚunreadzUnread: Útotal_unreadrž   Úprimaryz	 primary)Úneeds_replyzNeeds reply:u     â€” Úfromz: Úsubjecté2   Ú	importantz
Important:rÏ   Úmeetingsz
Meeting invites: r   Ú
z email digest sentu+    email digest skipped â€” no primary unreadzEmail digest failed: Tr@   )r’   r“   Úgenerate_briefingrW   rÈ   rp   r¦   r¶   r5   r6   r\   r[   r]   )r*   rH   Úgmailru   ÚlabelsÚlabelÚlinesr×   rÚ   r_   rÞ   rß   r,   r,   r-   rX     sJ   



 

(

("€ÿz%DashboardScheduler._send_email_digestc              
   C   sœ   z0ddl }| ¡ sW dS |jdd}|r)d|vr)d|vr)|  |¡ t d¡ W dS t d¡ W dS  tyM } ztjd	|› d
d W Y d}~dS d}~ww )z*Send new music releases on Friday morning.r   Né   ©ÚlimitzNo new dropsznot connectedzNew Music Friday sentz'New Music Friday: nothing new this weekzNew Music Friday failed: Tr@   )	Úspotify_moduleÚis_availableÚnew_releasesr¦   r5   r6   r\   r[   r]   )r*   ré   Úmusicr_   r,   r,   r-   rY   6  s   
"€ÿz"DashboardScheduler._send_new_musicc                    sp  z˜| j  ¡ }|d }t ¡ ‰ | dg ¡}t|ƒdkr5|d |d  }|dk r)dnd› d	t|ƒd
›d}nd}‡ fdd„| dg ¡D ƒ}‡ fdd„| dg ¡D ƒ}| di ¡ dd¡}t|tt	fƒrgd|› nt
|ƒ}	dˆ  d¡› dg}
|
 d¡ |
 d|d › d|› d¡ |
 d| dd¡› d| d d!¡› d"¡ |
 d#|	› ¡ | d$i ¡}| d%¡rÏ|
 d&|d% › d'| d(d!¡› d)| d*d!¡› d+| d,d¡› d-	¡ |rú|
 d¡ |
 d.¡ |D ]}| d/¡rëd	|d/ › nd}|
 d0|d1 › |› ¡ qÝ|r |
 d¡ |
 d2¡ |d3d4… D ]}|
 d0|d5 › d6|d1 › ¡ q|  ¡ }|rj|jrjz#| ¡ }| d7¡rM| d8¡rM|
 d¡ |
 d9¡ |
 |d8 ¡ W n tyi } zt d:|› ¡ W Y d3}~nd3}~ww |  ¡ }|r~|
 d¡ |
 d;|› ¡ |
 d¡ |
 d<¡ d= |
¡}|  |¡ t d>¡ W d3S  ty· } ztjd?|› d@dA W Y d3}~d3S d3}~ww )Bz#Send morning briefing via WhatsApp.ÚweightÚ	series30dr   éÿÿÿÿéùÿÿÿr   ÚdownÚupú ú.1fú lb this weekÚtrackingc                    ó"   g | ]}|  d ¡ˆ  ¡ kr|‘qS ©r   ©rW   rl   ©Ú.0r_   ©rm   r,   r-   Ú
<listcomp>Y  ó
    ÿz:DashboardScheduler._send_morning_nudge.<locals>.<listcomp>r…   c                    sB   g | ]}ˆ   ¡ | d d¡  k rˆ tdd   ¡ krn n|‘qS )r   r
   rÏ   ©Údays©rl   rW   r   rú   rü   r,   r-   rý   ]  s
    2ÿr`   ra   úN/Aú$u*   Good morning Bill â€” Luke here with your ú
%A, %B %-dz
 briefing:r
   zWeight: Úcurrentz lb (ú)zGoal pace: Úpacez lb/wk | ETA: ~ÚetaWeeksú?ú weekszCRO: rC   Útotal_hourszSleep: zh (Úbedtimeu   â†’Ú	wake_timeu
   ) Â· HRV: Úavg_hrvz mszTODAY:rB   ú  r†   z
COMING UP:NrÏ   r   õ    â€” r“   Úsummary_textzEMAIL:z(Gmail briefing in morning nudge failed: úON THIS DAY: u   Reply anytime â€” Luke's here.rà   zMorning nudge sentzMorning nudge failed: Tr@   )r   rj   r   rm   rW   rp   ÚabsÚ
isinstanceÚintÚfloatrr   ÚstrftimerÈ   r’   r“   rá   r[   r5   r\   Ú_get_on_this_dayr¶   r¦   r6   r]   )r*   ru   ÚwÚseriesÚweek_changeÚtrendÚevents_todayÚevents_upcomingÚcroÚcro_strrå   rC   r_   Útime_strrâ   Ú
email_dataÚinsightÚmessager,   rü   r-   rL   I  s|   
$

ÿ

ÿ 
(
@



"

€€ÿ




"€ÿz&DashboardScheduler._send_morning_nudgec              
      sf  z| j  ¡ }|d }t ¡ }d| d¡› g}| d¡ | d|d › d| dd	¡› d
¡ |tdd ‰ ‡ fdd„| dg ¡D ƒ}|rv| d¡ | dˆ  d¡› d¡ |D ]}| d¡rfd|d › nd}| d|d › |› ¡ qXn| d¡ | dˆ  d¡› d¡ | di ¡ dg ¡}|D ]e}	|	 dd¡}
zRt 	|
d|j
›  d¡ ¡ }||  kr¹|tdd krìn n1|	 dd¡}t|ttfƒrÎ|d | nd }|d krÖd!nd"› d#|› d$}| d%|
› d&|› d'|› ¡ W q“ ttfyø   Y q“w | d¡ | d(¡ d) |¡}|  |¡ t d*¡ W d.S  ty2 } ztjd+|› d,d- W Y d.}~d.S d.}~ww )/uW   Send evening check-in via WhatsApp. No weight nagging â€” Bill weighs in mornings only.rí   u   Evening check-in from Luke â€” r  r
   zToday's weight: r  ú lb | Pace: r  r  ú lb/wkrÎ   rÿ   c                    r÷   rø   rù   rú   ©Útomorrowr,   r-   rý   ª  rþ   z:DashboardScheduler._send_evening_nudge.<locals>.<listcomp>r…   z
TOMORROW (z%Az):rB   ró   r  r†   z Nothing scheduled for tomorrow (z).Ú
healthPlanÚweeklyCheckpointsr   ú%A, %B %d %Yé   r/   r	  r   Úabovezon track forz target ú lbz
Checkpoint alert: z target is u    lb â€” you're u?   Anything to update? (BP, events, signals, supplements) â€” Lukerà   zEvening nudge sentzEvening nudge failed: Tr@   N)r   rj   r   rm   r  rÈ   rW   r   r   ÚstrptimeÚyearr  r  r  Ú
ValueErrorÚ	TypeErrorr¶   r¦   r5   r6   r[   r]   )r*   ru   r  rm   rå   Útomorrow_eventsr_   r!  ÚcheckpointsÚcpÚcp_date_strÚcp_dater/   ÚdiffÚstatusr$  r,   r'  r-   rO     sV   

$

ÿ
þ
"€ÿ



"€ÿz&DashboardScheduler._send_evening_nudgec                    s"  zñ| j  ¡ }|d }|d }t ¡ ‰| dg ¡}t|ƒdkrRt|ƒdkr)|d n|d }|d }|| }t|dd… ƒd }t|ƒd	krI|d n|d }	||	 }
nd}|d
 }d}
ˆtdd  ¡ ‰‡‡fdd„| dg ¡D ƒ}ˆtdd  ¡ ‰ ‡ ‡fdd„| dg ¡D ƒ}dˆ 	d¡› g}| 
d¡ | 
d¡ | 
d¡ | 
d|d
 › d¡ | 
d|d›d¡ | 
d|d›d¡ | 
d|
d›d¡ | 
d|d › d| dd ¡› d!¡ | 
d"| d#d$¡› d%¡ | 
d¡ | 
d&¡ | 
d|d
 › d'|d › d(¡ | 
d)| d*d¡d›d+¡ | d,i ¡}| 
d¡ | 
d-¡ | 
d.| d/d$¡› d0| d1d$¡› d2| d3d4¡› d5¡ | 
d6| d7d ¡› ¡ | d8i ¡ d9¡}|r`| 
d¡ | 
d:|› ¡ |rs| 
d¡ | 
d;t|ƒ› d<¡ |rŸ| 
d¡ | 
d=t|ƒ› d>¡ |dd?… D ]}| 
d@|d7 › dA|dB › ¡ qŒzddl}|jd?dC}|r·| 
d¡ | 
|¡ W n
 tyÂ   Y nw |  ¡ }|r×| 
d¡ | 
dD|› ¡ | 
d¡ | 
dE¡ dF |¡}|  |¡ t dG¡ W dS  ty } ztjdH|› dIdJ W Y d}~dS d}~ww )Kz,Send weekly trend summary on Sunday morning.rí   ÚbodyFatrî   r   rð   r   rï   Né   r  rÿ   c                    s4   g | ]}ˆ |  d d¡  krˆ ¡ k rn n|‘qS ©r   r
   rù   rú   )Úpast_week_startrm   r,   r-   rý   î  ó
    $ÿz:DashboardScheduler._send_weekly_report.<locals>.<listcomp>r…   c                    s4   g | ]}ˆ  ¡ | d d¡  krˆ k rn n|‘qS r<  )rl   rW   rú   )Únext_week_endrm   r,   r-   rý   õ  r>  u(   WEEKLY DASHBOARD REPORT â€” Week ending z
%B %-d, %Yz(========================================r
   ÚWEIGHTz  Current: r.  z  This week: z+.1fz  7-day avg: rô   z  30-day change: z  Goal: Úgoalr%  r  r  r&  z  ETA: ~r  r	  r
  zBODY FATz	% (goal: z%)z
  Change: ÚdeltaVsLastú%ÚbloodPressurezBLOOD PRESSUREz  Last reading: Úsystolicú/Ú	diastolicrž   r9  Úunknownr  z  Date: r   r`   ra   zCRO: $zTHIS WEEK: z events completedzNEXT WEEK (z	 events):r:   r  r  r†   rç   r  u   Have a great week! â€” Lukerà   zWeekly report sentzWeekly report failed: Tr@   )r   rj   r   rm   rW   rp   Úsumr   rl   r  rÈ   ré   Úweekly_music_summaryr[   r  r¶   r¦   r5   r6   r]   )r*   ru   r  Úbfr  Ú
week_startÚweek_endr  Ú
weekly_avgÚmonth_startÚmonth_changeÚpast_eventsÚnext_eventsrå   Úbpr  r_   ré   rì   r#  r$  r,   )r?  r=  rm   r-   rS   Ô  sœ   


ÿ
ÿ


$

 

6


"

€ÿ




"€ÿz&DashboardScheduler._send_weekly_reportc              
   C   s,  zzt j t¡s
W dS t t¡}| ¡ }t ¡ }g }dD ]Q}zF|j	|j
| d}| ¡ dd… }| d|d f¡ | ¡ }|rb|d }	|d  	d	d
¡dd… }
|j
| }| d|› d|	› d|
› d¡ W q tyl   Y qw | ¡  |rx|d W S W dS  ty• } zt d|› ¡ W Y d}~dS d}~ww )zEQuery knowledge base for conversations from this date in prior years.N)rÎ   r,  rÏ   )r0  r9   aÁ  
                        SELECT c.title, substr(m.content, 1, 150), c.create_time
                        FROM messages m
                        JOIN conversations c ON c.id = m.conversation_id
                        WHERE m.role = 'user'
                        AND c.create_time LIKE ?
                        AND length(m.content) > 50
                        ORDER BY length(m.content) DESC
                        LIMIT 1
                    rC  r   rÎ   rà   ró   éd   ú(z) You were working on 'u   ' â€” "z..."zOn This Day lookup failed: )r³   r´   ÚexistsÚKNOWLEDGE_DB_PATHÚsqlite3ÚconnectÚcursorr   rm   Úreplacer0  rl   ÚexecuteÚfetchonerÈ   r1  Úcloser[   r5   rƒ   )r*   ÚconnÚcrm   ÚinsightsÚyear_offsetÚ	past_dateÚdate_prefixÚrowr†   Úsnippetr0  r_   r,   r,   r-   r  A  sB   
	÷
€ÿ
€þz#DashboardScheduler._get_on_this_dayc                    s2  zùddl m} |ƒ }|jst d¡ W dS g d¢}g }|D ]O}|j|dd}|s1|j|dd}|D ]9}|d |d	 | d
d¡| ¡ dœ}|dkrð| 	|d |d	 ¡}	|  
|¡}
|	dkrƒd|d< dg}|
rp| |
› d¡ n| d¡ | d¡ ||d< d|d< nå|	dkrÐddl m} | |d i ¡}| d¡dkr |d n|d	 }d|› d|d< dg}|
rº| |
› d¡ n| |› d¡ | d¡ ||d< d|d< n˜|	d krâd!|d< d"g|d< d#|d< n†d$|d< dg|d< d%|d< nx| |¡}| d&d'¡}| d(| ¡ ¡}||d)< |  
|¡}
|dkr7d*|d< d+g}|
r$| |
› d,¡ n| d-¡ | d.¡ ||d< d|d< n1|d'krVd/|d< d+g}| d0¡ | d.¡ ||d< d|d< n|d1krhd2|d< d3g|d< d#|d< | |¡ q3qddd'd1d4œ‰ |j‡ fd5d6„d7 z#dd8lm} ||ƒ}t d9t|ƒ› d:td;d<„ |D ƒƒ› d=¡ W |W S  tyÉ   t d>¡ t d9t|ƒ› d?td@d<„ |D ƒƒ› d=¡ Y |W S  tyú } z%t dA|› ¡ t d9t|ƒ› dBtdCd<„ |D ƒƒ› d=¡ W Y d}~|W S d}~ww  ty } ztjdD|› dEdF W Y d}~dS d}~ww )Gz•
        Fetch today's betting picks using Bill's Predictable Tempo strategy.
        Returns structured data for the dashboard Bet365 card.
        r   )Úget_analystz0Betting picks: Odds API not configured, skippingN©	ÚnhlÚeplÚserieaÚligue1ÚlaligaÚliga_portugalÚeuropaÚ
conferenceÚuclrÎ   ©Ú
days_aheadÚ	home_teamÚ	away_teamÚ	game_timer
   )ÚhomeÚawayrB   Úsportri  Ú
controlledu   â˜… CONTROLLEDÚtagzUnder 6.5 total goalsz ML (moneyline)z1st period Under 1.5z!Under 3.5 goals (1st two periods)ÚlegsÚprimeÚratingÚmixed)ÚNHL_TEAM_PROFILESÚtempoÚlowu
   MIXED â€” z anchorsz Under 3.5 team totalÚplayableÚchaoticu   âš¡ CHAOTICu"   Skip unders â€” high-event matchupÚavoidz	~ NEUTRALÚneutralÚtierr,  rä   ry  u
   â˜… TIER 1zUnder 2.5 goalsz to win (ML)zBTTS No (both teams to score)zUnder 8.5 cornersu   TIER 2 â€” filterzBTTS NorÏ   u	   âš  AVOIDu   Skip â€” unpredictable tempo)r}  rƒ  r†  r…  c                    s   ˆ   |   dd¡d¡S )Nr~  r†  r,  ©rW   )Úp©Úrating_orderr,   r-   Ú<lambda>Õ  s    z9DashboardScheduler._fetch_betting_picks.<locals>.<lambda>©Úkey)Úvalidate_picks_batchzBetting picks: z games processed + validated, c                 s   ó"    | ]}|  d ¡dkrdV  qdS ©r~  r}  rÎ   Nrˆ  ©rû   r‰  r,   r,   r-   Ú	<genexpr>Þ  ó   €  z:DashboardScheduler._fetch_betting_picks.<locals>.<genexpr>z prime targetsu3   pregame_validator not found â€” skipping validationz  games processed (unvalidated), c                 s   r  r‘  rˆ  r’  r,   r,   r-   r“  â  r”  u>   Pregame validation failed â€” picks pass through unvalidated: z% games processed (validation error), c                 s   r  r‘  rˆ  r’  r,   r,   r-   r“  æ  r”  zBetting picks fetch failed: Tr@   )Úsports_bettingrg  r“   r5   r6   Úget_upcoming_gamesÚget_upcoming_eventsrW   ÚupperÚclassify_nhl_matchupÚ_extract_favoriterÈ   r€  Úclassify_soccer_matchupÚsortÚpregame_validatorr  rp   rI  ÚImportErrorrƒ   r[   r]   )r*   rg  ÚanalystÚsportsry   Úsre   ÚgÚpickr  Úfavr|  r€  Úhome_pÚanchorÚleague_classr‡  rä   r  r_   r,   rŠ  r-   ro   s  sÐ   



ü





















¶Mÿ÷
ÿûÿ€û€þz'DashboardScheduler._fetch_betting_picksc                 C   sê  zÙddl m}m} |ƒ }|jsW dS d}g d¢}|D ]Z}|j|dd}|s,|j|dd}|D ]A}	z4|	 dd	¡}
|
s;W q.t |
 	d
d¡¡j	dd}|t 
¡   ¡ d }d|  k r\dkrcn nd}W  nW q. ttfyo   Y q.w |rt nq|szW dS t d¡ t ¡ | _zddlm} t|ƒ}| ¡  t d|› d¡ W n	 ty¦   Y nw |  ¡ }|dur×| j ¡ }|t ¡  ¡ dœ|d< | j |¡ t dt|ƒ› d¡ |  ¡  W dS W dS  tyô } zt d|› ¡ W Y d}~dS d}~ww )u+  
        Rapid pregame validation cycle â€” runs every 15 min when any game
        starts within 90 minutes. Clears the validation cache so fresh
        data (Claude analysis, scraping) is fetched for imminent games.
        Then re-fetches betting picks and updates briefing + dashboard.
        r   )rg  Ú
SPORT_KEYSNFrh  rÎ   rr  Úcommence_timer
   ÚZz+00:00)Útzinfoé<   éZ   TuE   RAPID PREGAME: Games within 90 min â€” running fresh validation cycle)Ú_validation_cachezRAPID PREGAME: Cleared z cached validationsrd   rg   z&RAPID PREGAME: Dashboard updated with z re-validated pickszRapid pregame refresh failed: ) r•  rg  r¨  r“   r–  r—  rW   r   Úfromisoformatr[  ÚutcnowÚtotal_secondsr1  r2  r5   r6   rB   r)   r  r®  rp   Úclearrž  ro   r   rj   rE   rl   ÚwriterD   r[   rƒ   )r*   rE   rg  r¨  rŸ  Úhas_imminentr   r¡  re   r¢  Úctrv  Úminutes_untilr®  Ú
cache_sizery   ru   r_   r,   r,   r-   rZ   î  sl   €ÿÿ

ÿ


þö€ÿz/DashboardScheduler._maybe_rapid_pregame_refreshc              
   C   sh   z&|  d¡r!|d d d   dg ¡}|r$t|dd„ d}|d W S W d	S W d	S  tttfy3   Y d	S w )
zGExtract the favorite team name from bookmaker data (internal use only).Ú
bookmakersr   ÚmarketsÚh2hc                 S   s   | d S )NÚpricer,   )Úxr,   r,   r-   rŒ  9  s    z6DashboardScheduler._extract_favorite.<locals>.<lambda>r  r   N)rW   ÚminÚKeyErrorÚ
IndexErrorr2  )r*   Úgamerº  r¤  r,   r,   r-   rš  3  s   

üûþþz$DashboardScheduler._extract_favoritec           "         s®  z·| j  ¡ }|d }t ¡ ‰ t ¡ }|j}|dk rd}n	|dk r$d}nd}| dg ¡}t|ƒdkrR|d	 |d
  }|dk r@dnd}|› dt	|ƒd›d}	|dk}
nd}	d}
‡ fdd„| dg ¡D ƒ}‡ fdd„| dg ¡D ƒdd… }d}| di ¡ dg ¡}|D ]O}zB| dd¡}t 
|dˆ j›  d¡ ¡ }ˆ |  kr¨ˆ tdd krÅn n| d d¡}t|d! | d"ƒ}||||d#kd$œ}W  nW q‚ ttfyÑ   Y q‚w | d%i ¡ d&¡}|  ¡ }d}|  ¡ }|r|jrz| ¡ }W n ty } zt d'|› ¡ W Y d}~nd}~ww |  ¡ }|r|jnd(|r|jnd(d)œ}|  ¡ }| ¡ |ˆ  d*¡|d! |	|
| d+d¡| d,d¡d-œ|||||| d.d¡| d/¡|||rWd0|indd1œ}ttƒjd2 }ddl}|jt |jƒd3d4\}} z1t! "|d5¡}!t#j$||!dd(d6 W d  ƒ n	1 sŽw   Y  t! %| t |ƒ¡ t &d7¡ W W dS  ty¸   t!j' (| ¡r·t! )| ¡ ‚ w  tyÖ } ztj*d8|› dd9 W Y d}~dS d}~ww ):z;Generate briefing data JSON for the dashboard morning card.rí   r=   zGood morningé   zGood afternoonzGood eveningrî   r   rï   rð   r   rñ   rò   ró   rô   rõ   rö   Tc                    s>   g | ]}|  d ¡ˆ  ¡ kr|d |  dd¡|  dd¡dœ‘qS )r   r†   rB   r
   r‡   )r†   rB   r‡   rù   rú   rü   r,   r-   rý   \  s
    þz<DashboardScheduler._update_briefing_data.<locals>.<listcomp>r…   c                    sZ   g | ])}ˆ   ¡ | d d¡  k rˆ tdd   ¡ krn n|d |d  | dd¡dœ‘qS )r   r
   rÏ   rÿ   r†   rB   )r†   r   rB   r  rú   rü   r,   r-   rý   c  s
    2þNr:   r)  r*  r   r
   r+  r,  rÿ   r/   r  rÎ   g      ð?)r   r/   r8  Úon_trackr`   ra   z%Gmail briefing for dashboard failed: F)Údriverâ   r  r  r  )r  r  Útrend_positiver  Ú	eta_weeksÚfocusrC   re   )Ú	generatedÚgreetingÚdate_displayrí   r  r  Ú
checkpointra   Úon_this_dayrÆ  rC   ÚemailÚgoogle_statusrg   úbriefing.jsonz	.json.tmp)ÚdirÚsuffixr  )ÚindentÚensure_asciizBriefing data updatedzBriefing update failed: r@   )+r   rj   r   rm   r   rE   rH   rW   rp   r  r/  r0  r   Úroundr1  r2  r  r’   r“   rá   r[   r5   r\   rŽ   ro   rl   r  r   r   ÚparentÚtempfileÚmkstemprr   r³   Úfdopenr‚   Údumpr[  r6   r´   rV  Úunlinkr]   )"r*   ru   r  rE   rH   rÈ  r  r  Útrend_directionÚ
trend_textrÄ  r  r  Úcheckpoint_alertr4  r5  r6  r7  r/   r8  ra   rË  Úemail_briefingrâ   r_   r–   rÍ  Úbetting_picksÚbriefingÚbriefing_pathrÕ  ÚfdÚtmp_pathÚfr,   rü   r-   rD   ?  sÈ   



þ

þü"ü€ÿ€ÿþ

û
ìÿ
ý"€ÿz(DashboardScheduler._update_briefing_datac              
   C   sÚ   t ttttgƒst d¡ dS zBdt› d}tj|ttfdt› dt› |dœdd}|j	d	v r>t 
d
t|ƒ› d¡ W dS t d|j	› d|jdd… › ¡ W dS  tyl } zt d|› ¡ W Y d}~dS d}~ww )z'Send a WhatsApp message via Twilio API.u<   Twilio credentials not configured â€” skipping WhatsApp sendNz+https://api.twilio.com/2010-04-01/Accounts/z/Messages.jsonz	whatsapp:)ÚFromÚToÚBodyræ   )Úauthru   ri   )r   éÉ   zWhatsApp message sent (z chars)zWhatsApp send failed: r  r   zWhatsApp send error: )Úallr	   r   r   r   r5   rƒ   r€   Úpostr   r6   rp   r]   Útextr[   )r*   r$  Úurlr„   r_   r,   r,   r-   r¦   À  s*   
ýø

(€ÿz!DashboardScheduler._send_whatsappc                 C   ó   |   ¡  dS )z;Manually trigger morning briefing (callable from WhatsApp).zMorning briefing sent!)rL   r7   r,   r,   r-   Útrigger_morning_briefingÛ  ó   z+DashboardScheduler.trigger_morning_briefingc                 C   rí  )z8Manually trigger weekly report (callable from WhatsApp).zWeekly report sent!)rS   r7   r,   r,   r-   Útrigger_weekly_reportà  rï  z(DashboardScheduler.trigger_weekly_reportc                 C   s   |   ¡ }|pdS )z1Get on-this-day insight (callable from WhatsApp).z8No conversations found from this date in previous years.)r  )r*   r#  r,   r,   r-   Úget_on_this_dayå  s   z"DashboardScheduler.get_on_this_dayc              
   C   s¾   t  d¡ z2ddlm} t  d¡ || jƒ}t  dt|ƒdd… › ¡ |r3d| ¡ vr3|  ¡  |W S |p6d	W S  ty^ } zt j	d
|› dd dt|ƒdd… › W  Y d}~S d}~ww )z<Manually trigger Oura API data pull. Returns summary string.ztrigger_oura_ingest: startingr   rÃ   z)trigger_oura_ingest: oura_api imported OKztrigger_oura_ingest: result = NrÐ   rÅ   zNo data returned from Oura API.zOura manual ingest failed: Tr@   zOura API error: rT  )
r5   r6   rÆ   rÄ   r   rr   rÇ   rD   r[   r]   )r*   rÄ   r¬   r_   r,   r,   r-   Útrigger_oura_ingestê  s   



"€þz&DashboardScheduler.trigger_oura_ingestc              
   C   s^  |   ¡ }|rE|jrEz!|jdd}|r)|  |¡}|r)| j |¡ |  ¡  d|› W S W n tyD } zt 	d|› ¡ W Y d}~nd}~ww t
gt }tttƒjƒ}| |¡ d}d}|D ]9}	tj |	¡seq\tj |	d¡tj |	d¡tj |	d	d¡fD ]}
tj|
d
dD ]}tj |¡}||kr“|}|}qƒqzq\|sšdS |  |¡}|r­| j |¡ |  ¡  |S dS )zRManually trigger Health Connect ingestion. Tries Drive API first, then filesystem.éH   r˜   z(via Google Drive)
z)Drive fetch failed during manual ingest: Nr   r­   r®   r¯   Tr°   zCNo Health Connect zip files found in Google Drive or local folders.z3Health Connect ingestion failed. Check server logs.)rŽ   r“   Úfetch_latest_health_connectr¤   r"   r¥   rD   r[   r5   rƒ   r   r²   rr   r   r   rÔ  rÈ   r³   r´   rµ   r¶   r·   r¸   )r*   r–   r«   r¬   r_   rº   Údash_dirÚlatest_fileÚlatest_mtimer»   r½   r¾   r¿   r,   r,   r-   rÒ   ú  sV   

€€ÿ

ý€üû
z(DashboardScheduler.trigger_health_ingestc                 C   sL   t tƒjd }| ¡ r$t|ƒ}t |¡W  d  ƒ S 1 sw   Y  dS )z%Return current briefing data as dict.rÎ  N)r   r   rÔ  rV  Úopenr‚   Úload)r*   rà  rã  r,   r,   r-   Úget_briefing_data+  s   
 ÿz$DashboardScheduler.get_briefing_dataN)%Ú__name__Ú
__module__Ú__qualname__Ú__doc__r.   r4   r8   r3   rG   rk   rn   rŽ   r’   rU   r”   r•   r¤   rV   rÉ   Ú_daily_health_connect_grabrX   rY   rL   rO   rS   r  ro   rZ   rš  rD   r¦   rî  rð  rñ  rò  rÒ   rú  r,   r,   r,   r-   r   ?   sH    	\5,"164T7m2{E 1r   )'rþ  r³   r‚   r·   rX  Úloggingr1   rB   r€   r   r   r   Úpathlibr   Ú	getLoggerr5   Úgetenvrr   Ú__file__ÚresolverÔ  r   rW  r	   r   r   r   rI   rK   rM   rN   rQ   rR   rF   rT   rw  r   r²   r   r,   r,   r,   r-   Ú<module>   sL    
þþý