feat(tempo): blue team Tempo time from pick-up, not queue entry
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Previously blue_started_at was set when the RED team submitted evidence
(= queue open time), so Tempo was getting total queue wait time instead
of actual work time.
Changes:
- DB: add blue_work_started_at column (migration b045), set when a blue
tech explicitly picks up the test (mirrors red_started_at for red team)
- Workflow: new start_blue_work() function + POST /tests/{id}/start-blue-work
endpoint (blue_tech / blue_lead roles). Cannot be called twice.
- submit_blue_evidence: uses blue_work_started_at (when available) as the
phase start for the Tempo worklog, falls back to blue_started_at
- reopen_test: clears blue_work_started_at alongside other timing fields
- Tempo: both red_team_execution and blue_team_evaluation now synced;
correct work_date and description per activity type
- Frontend: "Start Evaluation" button shown in blue_evaluating state when
blue_work_started_at is null; live timer shows from pick-up time
What each timestamp tracks:
blue_started_at = queue entry (SLA / internal tracking)
blue_work_started_at = pick-up by blue tech (Tempo start)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -187,11 +187,46 @@ def submit_red_evidence(db: Session, test: Test, user: User) -> Test:
|
||||
return test
|
||||
|
||||
|
||||
def start_blue_work(db: Session, test: Test, user: User) -> Test:
|
||||
"""Mark that a blue tech has picked up this test to start evaluating.
|
||||
|
||||
Sets blue_work_started_at. Only valid in blue_evaluating state and
|
||||
only if blue_work_started_at is not already set.
|
||||
"""
|
||||
if test.state != TestState.blue_evaluating:
|
||||
raise InvalidOperationError(
|
||||
f"Cannot start blue work in '{test.state.value}' state"
|
||||
)
|
||||
if test.blue_work_started_at is not None:
|
||||
raise InvalidOperationError("Blue work already started")
|
||||
|
||||
test.blue_work_started_at = datetime.utcnow()
|
||||
db.flush()
|
||||
|
||||
log_action(
|
||||
db,
|
||||
user_id=user.id,
|
||||
action="start_blue_work",
|
||||
entity_type="test",
|
||||
entity_id=test.id,
|
||||
details={"test_name": test.name},
|
||||
)
|
||||
|
||||
try:
|
||||
notify_test_state_change(db, test, "blue_work_started")
|
||||
except Exception as e:
|
||||
logger.warning("Notification failed for test %s: %s", test.id, e, exc_info=True)
|
||||
|
||||
return test
|
||||
|
||||
|
||||
def submit_blue_evidence(db: Session, test: Test, user: User) -> Test:
|
||||
"""Move from ``blue_evaluating`` → ``in_review``.
|
||||
|
||||
Called by **blue_tech** once they have finished documenting detection.
|
||||
Stops the Blue Team timer and creates an automatic worklog.
|
||||
Uses blue_work_started_at as the phase start for Tempo if available,
|
||||
otherwise falls back to blue_started_at (queue-entry timestamp).
|
||||
"""
|
||||
now = datetime.utcnow()
|
||||
|
||||
@@ -206,12 +241,14 @@ def submit_blue_evidence(db: Session, test: Test, user: User) -> Test:
|
||||
action_name="submit_blue_evidence",
|
||||
)
|
||||
|
||||
# Create automatic worklog for Blue Team phase (subtract paused time)
|
||||
# Create automatic worklog for Blue Team phase (subtract paused time).
|
||||
# Use blue_work_started_at (actual pick-up time) when available so the
|
||||
# Tempo worklog reflects real working time, not just queue time.
|
||||
_create_phase_worklog(
|
||||
db,
|
||||
test=test,
|
||||
user=user,
|
||||
phase_started_at=test.blue_started_at,
|
||||
phase_started_at=test.blue_work_started_at or test.blue_started_at,
|
||||
phase_ended_at=now,
|
||||
paused_seconds=(test.blue_paused_seconds or 0) + paused_extra,
|
||||
activity_type="blue_team_evaluation",
|
||||
@@ -612,6 +649,7 @@ def reopen_test(db: Session, test: Test, user: User) -> Test:
|
||||
# Clear phase timing fields
|
||||
test.red_started_at = None
|
||||
test.blue_started_at = None
|
||||
test.blue_work_started_at = None
|
||||
test.paused_at = None
|
||||
test.red_paused_seconds = 0
|
||||
test.blue_paused_seconds = 0
|
||||
|
||||
Reference in New Issue
Block a user