@@ -124,31 +124,28 @@ def compile(ctx: typer.Context,
124124 raise typer .Exit (1 )
125125
126126 for aud in audiences :
127- compiled_timesheet = aud .compile_time_sheet (log )
128- if compiled_timesheet :
129- # Sign the timesheet if signing_ids are configured
130- signing_ids = aud .config .get ('signing_ids' , [])
131- if signing_ids :
132- signed = False
133- for signing_id in signing_ids :
134- key = ws .identities .get_identity (signing_id )
135- if key :
136- compiled_timesheet = compiled_timesheet .sign (signing_id , bytes (key ))
137- signed = True
138- else :
139- typer .echo (f"Warning: No identity key found for { signing_id } " , err = True )
140-
141- if signed :
142- ws .timesheets .write_timesheet (compiled_timesheet )
143- typer .echo (f"Compiled and signed timesheet for { resolved_date } using { aud .id } ." )
127+ compiled_timesheet = ws .timesheets .compile (log , aud )
128+ # Sign the timesheet if signing_ids are configured
129+ signing_ids = aud .config .get ('signing_ids' , [])
130+ if signing_ids :
131+ signed = False
132+ for signing_id in signing_ids :
133+ key = ws .identities .get_identity (signing_id )
134+ if key :
135+ compiled_timesheet = compiled_timesheet .sign (signing_id , bytes (key ))
136+ signed = True
144137 else :
145- ws .timesheets .write_timesheet (compiled_timesheet )
146- typer .echo (f"Warning: Compiled unsigned timesheet for { resolved_date } using { aud .id } (no valid signing keys)" , err = True )
138+ typer .echo (f"Warning: No identity key found for { signing_id } " , err = True )
139+
140+ if signed :
141+ ws .timesheets .write_timesheet (compiled_timesheet )
142+ typer .echo (f"Compiled and signed timesheet for { resolved_date } using { aud .id } ." )
147143 else :
148144 ws .timesheets .write_timesheet (compiled_timesheet )
149- typer .echo (f"Warning: Compiled unsigned timesheet for { resolved_date } using { aud .id } (no signing_ids configured )" , err = True )
145+ typer .echo (f"Warning: Compiled unsigned timesheet for { resolved_date } using { aud .id } (no valid signing keys )" , err = True )
150146 else :
151- typer .echo (f"No timesheet for { resolved_date } from { aud .id } (no relevant sessions)." )
147+ ws .timesheets .write_timesheet (compiled_timesheet )
148+ typer .echo (f"Warning: Compiled unsigned timesheet for { resolved_date } using { aud .id } (no signing_ids configured)" , err = True )
152149 else :
153150 # No date provided - find all logs that need compiling
154151 log_dates = ws .logs .list_log_dates ()
@@ -173,7 +170,7 @@ def compile(ctx: typer.Context,
173170
174171 for aud in audiences :
175172 if (aud .id , log_date ) not in existing :
176- compiled_timesheet = aud . compile_time_sheet (log )
173+ compiled_timesheet = ws . timesheets . compile (log , aud )
177174 # Sign the timesheet if signing_ids are configured (even if empty)
178175 is_empty = len (compiled_timesheet .timeline ) == 0
179176 signing_ids = aud .config .get ('signing_ids' , [])
@@ -332,6 +329,58 @@ def status(ctx: typer.Context):
332329 elif not has_unclosed :
333330 typer .echo ("All logs have compiled timesheets ✓" )
334331
332+ # Check for stale timesheets (log changed after compilation)
333+ typer .echo ("\n --- Stale timesheets (log changed) ---" )
334+ stale = []
335+ for ts in existing_timesheets :
336+ try :
337+ # Get the current log and calculate its hash
338+ raw_log = ws .logs .read_log_raw (ts .date )
339+ from faff_core .models import Log
340+ current_hash = Log .calculate_hash (raw_log )
341+
342+ # Compare with the hash stored in the timesheet
343+ if ts .meta .log_hash and ts .meta .log_hash != current_hash :
344+ stale .append (ts )
345+ except :
346+ # Log might not exist anymore
347+ pass
348+
349+ if stale :
350+ # Group by audience
351+ by_audience = {}
352+ for ts in stale :
353+ if ts .meta .audience_id not in by_audience :
354+ by_audience [ts .meta .audience_id ] = []
355+ by_audience [ts .meta .audience_id ].append (ts )
356+
357+ for audience_id , timesheets in by_audience .items ():
358+ typer .echo (f"For { audience_id } :" )
359+ for ts in sorted (timesheets , key = lambda t : t .date ):
360+ hours = sum (s .duration .total_seconds () for s in ts .timeline ) / 3600
361+ typer .echo (f" ⚠️ { ts .date } : { hours :.2f} h (recompile needed)" )
362+ typer .echo (f" Total: { len (stale )} stale timesheet(s)" )
363+ else :
364+ typer .echo ("All timesheets are up-to-date ✓" )
365+
366+ # Check for failed submissions
367+ typer .echo ("\n --- Failed submissions ---" )
368+ failed = [ts for ts in existing_timesheets if ts .meta .submission_status == "failed" ]
369+
370+ if failed :
371+ for ts in sorted (failed , key = lambda t : t .date ):
372+ hours = sum (s .duration .total_seconds () for s in ts .timeline ) / 3600
373+ typer .echo (f"❌ { ts .meta .audience_id } - { ts .date } : { hours :.2f} h" )
374+ if ts .meta .submission_error :
375+ # Truncate long error messages
376+ error = ts .meta .submission_error
377+ if len (error ) > 100 :
378+ error = error [:97 ] + "..."
379+ typer .echo (f" Error: { error } " )
380+ typer .echo (f" Total: { len (failed )} failed submission(s)" )
381+ else :
382+ typer .echo ("No failed submissions ✓" )
383+
335384 # Check what needs pushing
336385 typer .echo ("\n --- Timesheets needing submission ---" )
337386 unsubmitted = [ts for ts in existing_timesheets if ts .meta .submitted_at is None ]
0 commit comments