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:
@@ -51,6 +51,7 @@ interface TestDetailHeaderProps {
|
||||
onStartExecution: () => void;
|
||||
onSubmitRed: () => void;
|
||||
onSubmitBlue: () => void;
|
||||
onStartBlueWork: () => void;
|
||||
onOpenValidateModal: (side: "red" | "blue") => void;
|
||||
onReopen: () => void;
|
||||
onPauseTimer: () => void;
|
||||
@@ -67,6 +68,7 @@ export default function TestDetailHeader({
|
||||
onStartExecution,
|
||||
onSubmitRed,
|
||||
onSubmitBlue,
|
||||
onStartBlueWork,
|
||||
onOpenValidateModal,
|
||||
onReopen,
|
||||
onPauseTimer,
|
||||
@@ -127,22 +129,38 @@ export default function TestDetailHeader({
|
||||
);
|
||||
}
|
||||
|
||||
// Blue Team in blue_evaluating -> Submit for Review
|
||||
// Blue Team in blue_evaluating:
|
||||
// - if not picked up yet: show "Start Evaluation" button
|
||||
// - if already picked up: show "Submit for Review" button
|
||||
if (
|
||||
test.state === "blue_evaluating" &&
|
||||
(role === "blue_tech" || role === "blue_lead" || role === "admin")
|
||||
) {
|
||||
buttons.push(
|
||||
<button
|
||||
key="submit-blue"
|
||||
onClick={onSubmitBlue}
|
||||
disabled={isTransitioning}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isTransitioning ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
Submit for Review
|
||||
</button>,
|
||||
);
|
||||
if (!test.blue_work_started_at) {
|
||||
buttons.push(
|
||||
<button
|
||||
key="start-blue-work"
|
||||
onClick={onStartBlueWork}
|
||||
disabled={isTransitioning}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isTransitioning ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />}
|
||||
Start Evaluation
|
||||
</button>,
|
||||
);
|
||||
} else {
|
||||
buttons.push(
|
||||
<button
|
||||
key="submit-blue"
|
||||
onClick={onSubmitBlue}
|
||||
disabled={isTransitioning}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isTransitioning ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
Submit for Review
|
||||
</button>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Red Lead in in_review -> Validate Red
|
||||
@@ -264,10 +282,10 @@ export default function TestDetailHeader({
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (test.state === "blue_evaluating" && test.blue_started_at) {
|
||||
if (test.state === "blue_evaluating" && test.blue_work_started_at) {
|
||||
return (
|
||||
<LiveTimer
|
||||
startedAt={test.blue_started_at}
|
||||
startedAt={test.blue_work_started_at}
|
||||
pausedAt={test.paused_at}
|
||||
pausedSeconds={test.blue_paused_seconds}
|
||||
label="Blue Team Timer"
|
||||
|
||||
Reference in New Issue
Block a user