@@ -123,8 +123,14 @@ async function runHeartbeat(env) {
123123 if ( ! state . historyByService ) state . historyByService = { } ;
124124
125125 let statusChanged = false ;
126+ let needsSave = false ;
126127 let notifications = [ ] ;
127128 const heartbeatAt = Date . now ( ) ;
129+ const todayUTC = new Date ( heartbeatAt ) . toISOString ( ) . slice ( 0 , 10 ) ; // "YYYY-MM-DD"
130+
131+ // Save at most every 15 minutes for latency/history freshness (~96 writes/day on free tier)
132+ const PERIODIC_SAVE_INTERVAL_MS = 15 * 60 * 1000 ;
133+ const isPeriodicSave = heartbeatAt - ( state . _lastSaveTime || 0 ) >= PERIODIC_SAVE_INTERVAL_MS ;
128134
129135 for ( const target of targets ) {
130136 if ( state . services [ target . id ] ?. isManual ) {
@@ -168,25 +174,56 @@ async function runHeartbeat(env) {
168174 status : newStatus ,
169175 latency : latency
170176 } ) ;
177+
178+ // Internal log every heartbeat (no KV write)
179+ console . log ( `[heartbeat] ${ target . name } : ${ newStatus } ${ latency !== null ? ` (${ latency } ms)` : '' } ` ) ;
171180 }
172181
173- // Fetch maintained issues from ProjectHub
174- try {
175- const issuesRes = await fetch ( 'https://api.github.com/repos/SillyLittleTech/projecthub/issues?labels=maintain&state=open' , {
176- headers : {
177- 'User-Agent' : 'SLT-Status-Worker' ,
178- 'Authorization' : env . ISSUES_PAT ? `Bearer ${ env . ISSUES_PAT } ` : ''
182+ // Fetch maintained issues from ProjectHub at most once per day.
183+ // Track attempt day separately from success day so a non-2xx response
184+ // doesn't cause unbounded retries on every heartbeat.
185+ const lastIssuesFetchAttempt = state . _lastIssuesFetchAttemptDay || '' ;
186+ if ( lastIssuesFetchAttempt !== todayUTC ) {
187+ state . _lastIssuesFetchAttemptDay = todayUTC ; // record attempt regardless of outcome
188+ needsSave = true ; // persist the attempt date so retries are bounded
189+ try {
190+ const issuesRes = await fetch ( 'https://api.github.com/repos/SillyLittleTech/projecthub/issues?labels=maintain&state=open' , {
191+ headers : {
192+ 'User-Agent' : 'SLT-Status-Worker' ,
193+ 'Authorization' : env . ISSUES_PAT ? `Bearer ${ env . ISSUES_PAT } ` : ''
194+ }
195+ } ) ;
196+ if ( issuesRes . ok ) {
197+ const issues = await issuesRes . json ( ) ;
198+ state . maintenance = issues . map ( i => ( { title : i . title , url : i . html_url , created_at : i . created_at } ) ) ;
199+ state . _lastIssuesFetchDay = todayUTC ;
200+ console . log ( `[daily] Fetched ${ issues . length } maintenance issue(s) for ${ todayUTC } ` ) ;
201+ } else {
202+ console . error ( `[daily] Issues fetch failed: HTTP ${ issuesRes . status } for ${ todayUTC } ` ) ;
179203 }
180- } ) ;
181- if ( issuesRes . ok ) {
182- const issues = await issuesRes . json ( ) ;
183- state . maintenance = issues . map ( i => ( { title : i . title , url : i . html_url , created_at : i . created_at } ) ) ;
204+ } catch ( e ) {
205+ console . error ( 'Failed to fetch maintenance issues' , e ) ;
184206 }
185- } catch ( e ) {
186- console . error ( 'Failed to fetch maintenance issues' , e ) ;
187207 }
188208
189- await saveState ( env , state ) ;
209+ // Only write to KV when something meaningful changed:
210+ // - status change: immediate (for accurate frontend display)
211+ // - periodic (every 15 min): keeps latency/history reasonably fresh (~96 writes/day)
212+ // - daily: ensures a flush even during quiet periods + logs a summary
213+ // - needsSave: issues fetch attempt date or maintenance list must be persisted
214+ const lastDailySaveDate = state . _lastDailySaveDay || '' ;
215+ const isDailySave = lastDailySaveDate !== todayUTC ;
216+ if ( statusChanged || needsSave || isDailySave || isPeriodicSave ) {
217+ state . _lastSaveTime = heartbeatAt ; // reset the 15-min clock on any save
218+ if ( isDailySave ) {
219+ state . _lastDailySaveDay = todayUTC ;
220+ const summary = Object . entries ( state . services )
221+ . map ( ( [ , svc ] ) => `${ svc . name } : ${ svc . status } ` )
222+ . join ( ', ' ) ;
223+ console . log ( `[daily summary] ${ todayUTC } — ${ summary } ` ) ;
224+ }
225+ await saveState ( env , state ) ;
226+ }
190227}
191228
192229async function saveState ( env , state ) {
0 commit comments