Skip to content

Commit 9946e75

Browse files
committed
Implement some more robust state tracking of timesheet submissions
1 parent 58adc5a commit 9946e75

File tree

2 files changed

+72
-23
lines changed

2 files changed

+72
-23
lines changed

src/faff_cli/main.py

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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]

src/faff_cli/timesheet.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def compile(ctx: typer.Context, date: str = typer.Argument(None)):
2929

3030
compilers = ws.timesheets.audiences()
3131
for compiler in compilers:
32-
compiled_timesheet = compiler.compile_time_sheet(log)
32+
compiled_timesheet = ws.timesheets.compile(log, compiler)
3333

3434
# Sign the timesheet if signing_ids are configured (even if empty)
3535
signing_ids = compiler.config.get('signing_ids', [])

0 commit comments

Comments
 (0)