@@ -181,18 +181,18 @@ def norm(s):
181181agents = sorted ({e ["agent" ] for e in events })
182182ts_min , ts_max = Q ("SELECT MIN(ts),MAX(ts) FROM position_state_transitions" )[0 ]
183183
184- # ── usage giornaliero (% del budget SETTIMANALE AI consumato per giorno) ──
185- # Da sentinel-data.jsonl: weekly_usage è cumulativo dentro la settimana (cresce
186- # 0→~100) e si azzera al reset (Gio ~06:00 UTC). Consumo del giorno = valore di
187- # FINE giornata − fine del giorno precedente nella STESSA settimana (0 se è il
188- # primo giorno della settimana). Robusto all'oscillazione intraday; ogni
189- # settimana completa somma ~100%.
190- def _week_key (day_iso ): # giovedì di riferimento (settimana di budget)
191- d = datetime .date .fromisoformat (day_iso )
192- return (d - datetime .timedelta (days = (d .weekday () - 3 ) % 7 )).isoformat ()
184+ # ── usage giornaliero (% del budget AI consumato per giorno) ──
185+ # Da sentinel-data.jsonl: weekly_usage è cumulativo dentro il ciclo di budget
186+ # (cresce 0→~100) e si azzera al reset. Il reset NON è solo il giovedì di
187+ # calendario: l'utente può prendere un nuovo abbonamento e ripartire da 0 senza
188+ # aspettare la settimana. Quindi i CICLI si rilevano dai dati: nuovo ciclo quando
189+ # il cum SCENDE (reset, naturale o nuovo abbonamento) o c'è un buco di giorni.
190+ # Consumo del giorno = fine giornata − fine del giorno prima NELLO STESSO ciclo
191+ # (= il valore stesso se è il primo giorno del ciclo). `week` = data inizio ciclo.
193192
194193SENTINEL = "/root/.jht/logs/sentinel-data.jsonl"
195194usage = None
195+ hour_cum = {} # "YYYY-MM-DDTHH" -> budget cumulato % (ultimo sample dell'ora)
196196if os .path .exists (SENTINEL ):
197197 samples = [] # (ts, weekly_usage)
198198 provider = "codex"
@@ -211,18 +211,29 @@ def _week_key(day_iso): # giovedì di riferimento (settimana di budget)
211211 if e .get ("provider" ):
212212 provider = e ["provider" ]
213213 samples .sort ()
214+ for ts , wu in samples : # budget cum per ORA (ultimo sample dell'ora vince)
215+ hour_cum [ts [:13 ]] = wu
214216 day_last = collections .OrderedDict () # day -> weekly_usage di fine giornata
215217 for ts , wu in samples :
216218 day_last [ts [:10 ]] = wu # l'ultimo sample del giorno sovrascrive
217219 daily = []
218- week_prev = {} # week_key -> fine del giorno precedente (nella settimana)
220+ prev_day = None
221+ prev_end = None
222+ cycle = None # data di inizio del ciclo di budget corrente
219223 for day , end in day_last .items ():
220- wk = _week_key (day )
221- pct = end - week_prev .get (wk , 0 )
222- week_prev [wk ] = end
223- # pct = consumo del giorno; cum = totale settimanale a fine giornata
224+ gap = (prev_day is not None and
225+ (datetime .date .fromisoformat (day )
226+ - datetime .date .fromisoformat (prev_day )).days > 1 )
227+ reset = prev_end is not None and end < prev_end - 1 # cum sceso = reset
228+ if cycle is None or reset or gap :
229+ cycle , base = day , 0.0 # nuovo ciclo: riparte da 0
230+ else :
231+ base = prev_end
232+ pct = end - base
233+ # pct = consumo del giorno; cum = totale del ciclo a fine giornata
224234 daily .append ({"day" : day , "pct" : round (max (pct , 0 ), 1 ),
225- "cum" : round (end , 1 ), "week" : wk })
235+ "cum" : round (end , 1 ), "week" : cycle })
236+ prev_day , prev_end = day , end
226237 # orario di lavoro configurato (contesto: su quali ore/giorni si spalma)
227238 working_hours = None
228239 CONFIG = "/root/.jht/jht.config.json"
@@ -243,6 +254,53 @@ def _week_key(day_iso): # giovedì di riferimento (settimana di budget)
243254 usage = {"provider" : provider , "unit" : "weekly_budget_pct" ,
244255 "daily" : daily , "workingHours" : working_hours }
245256
257+ # ── attività + budget per ORA (per viste intraday su fasi corte, es. burst free-run) ──
258+ # Solo le ore con almeno una transizione (compatto). Ogni bucket:
259+ # {hour:"YYYY-MM-DDTHH", counts:{ruolo:n}, cum: budget% a quell'ora}. Il cum è
260+ # riportato in avanti dall'ultimo sample noto, così le ore senza sample budget
261+ # non azzerano la linea.
262+ hour_acts = collections .OrderedDict () # hour -> {role: n}
263+ for e in events :
264+ hr = e ["ts" ][:13 ] # "YYYY-MM-DDTHH"
265+ role = e ["agent" ].split ("-" )[0 ]
266+ b = hour_acts .setdefault (hr , {})
267+ b [role ] = b .get (role , 0 ) + 1
268+ hourly = []
269+ _last_cum = 0.0
270+ for hr in sorted (hour_acts ):
271+ if hr in hour_cum :
272+ _last_cum = hour_cum [hr ]
273+ hourly .append ({"hour" : hr , "counts" : hour_acts [hr ], "cum" : round (_last_cum , 1 )})
274+
275+ # ── funnel TROVATE → ESCLUSE / TENUTE (per giorno + totali) ──────────
276+ # Per ogni giorno di found_at: quante posizioni trovate e come sono finite
277+ # (status attuale). "tenute" = non escluse. Serve a mostrare la proporzione
278+ # escluse vs tenute giorno per giorno (e in totale, per il donut).
279+ fd_rows = Q (
280+ "SELECT substr(found_at,1,10) d, status, COUNT(*) "
281+ "FROM positions WHERE found_at IS NOT NULL AND TRIM(found_at)<>'' "
282+ "GROUP BY 1, 2" )
283+ fd = collections .OrderedDict () # day -> {status: n}
284+ for d , st , n in fd_rows :
285+ fd .setdefault (d , {})[(st or "?" )] = n
286+ funnel_daily = []
287+ for d in sorted (fd ):
288+ sm = fd [d ]
289+ excl = sm .get ("excluded" , 0 )
290+ found = sum (sm .values ())
291+ funnel_daily .append ({
292+ "day" : d , "found" : found , "excluded" : excl , "kept" : found - excl ,
293+ "scored" : sm .get ("scored" , 0 ), "ready" : sm .get ("ready" , 0 ),
294+ })
295+ status_tot = {(st or "?" ): n for st , n in Q (
296+ "SELECT status, COUNT(*) FROM positions GROUP BY status" )}
297+ _excl_tot = status_tot .get ("excluded" , 0 )
298+ _found_tot = sum (status_tot .values ())
299+ funnel_totals = {
300+ "found" : _found_tot , "excluded" : _excl_tot , "kept" : _found_tot - _excl_tot ,
301+ "scored" : status_tot .get ("scored" , 0 ), "ready" : status_tot .get ("ready" , 0 ),
302+ }
303+
246304out = {
247305 "source" : "andris-codex" ,
248306 "tsRange" : [ts_min , ts_max ],
@@ -259,6 +317,9 @@ def _week_key(day_iso): # giovedì di riferimento (settimana di budget)
259317 "salary" : salary ,
260318 "agents" : agents ,
261319 "events" : events ,
320+ "hourly" : hourly ,
321+ "funnelDaily" : funnel_daily ,
322+ "funnelTotals" : funnel_totals ,
262323 "usage" : usage ,
263324}
264325print (json .dumps (out , ensure_ascii = False , indent = 2 ))
0 commit comments