showing previous version schedule as faded
This commit is contained in:
+19
-1
@@ -406,6 +406,7 @@ def compile_schedule_endpoint(request: CompileScheduleRequest) -> dict[str, Any]
|
|||||||
top_priority_pairs: set[tuple[str, str]] = set()
|
top_priority_pairs: set[tuple[str, str]] = set()
|
||||||
dut_top_priority_ids = {item.strip() for item in request.top_priority_tests_dut if item.strip()}
|
dut_top_priority_ids = {item.strip() for item in request.top_priority_tests_dut if item.strip()}
|
||||||
ref_top_priority_ids = {item.strip() for item in request.top_priority_tests_ref if item.strip()}
|
ref_top_priority_ids = {item.strip() for item in request.top_priority_tests_ref if item.strip()}
|
||||||
|
status_lookup = {(test.test_id, test.device): test.status for test in stored_tests}
|
||||||
|
|
||||||
# Backward compatibility: if only legacy top_priority_tests was sent,
|
# Backward compatibility: if only legacy top_priority_tests was sent,
|
||||||
# keep previous behavior by applying IDs to whichever device has that test.
|
# keep previous behavior by applying IDs to whichever device has that test.
|
||||||
@@ -445,7 +446,17 @@ def compile_schedule_endpoint(request: CompileScheduleRequest) -> dict[str, Any]
|
|||||||
raise HTTPException(status_code=409, detail=schedule_error)
|
raise HTTPException(status_code=409, detail=schedule_error)
|
||||||
|
|
||||||
version = db.create_schedule_version(
|
version = db.create_schedule_version(
|
||||||
[(e.test_id, e.device, e.scheduled_date, e.shift_index, e.sequence_in_shift) for e in entries],
|
[
|
||||||
|
(
|
||||||
|
e.test_id,
|
||||||
|
e.device,
|
||||||
|
e.scheduled_date,
|
||||||
|
e.shift_index,
|
||||||
|
e.sequence_in_shift,
|
||||||
|
status_lookup.get((e.test_id, e.device), "pending"),
|
||||||
|
)
|
||||||
|
for e in entries
|
||||||
|
],
|
||||||
DB_PATH,
|
DB_PATH,
|
||||||
)
|
)
|
||||||
print(f"Schedule version {version} created with {len(entries)} entries, completion date: {completion_date}")
|
print(f"Schedule version {version} created with {len(entries)} entries, completion date: {completion_date}")
|
||||||
@@ -506,10 +517,17 @@ def get_schedule_week(start: str | None = None, version: int | None = None) -> d
|
|||||||
|
|
||||||
all_rows = db.get_schedule_rows(selected_version, DB_PATH)
|
all_rows = db.get_schedule_rows(selected_version, DB_PATH)
|
||||||
completion_date = max((row.scheduled_date for row in all_rows), default=None)
|
completion_date = max((row.scheduled_date for row in all_rows), default=None)
|
||||||
|
start_shift = min(
|
||||||
|
((row.scheduled_date, row.shift_index) for row in all_rows),
|
||||||
|
default=(None, None),
|
||||||
|
)
|
||||||
|
schedule_start_date, schedule_start_shift_index = start_shift
|
||||||
holiday_dates = db.list_holidays(DB_PATH)
|
holiday_dates = db.list_holidays(DB_PATH)
|
||||||
return {
|
return {
|
||||||
"start_date": week_start,
|
"start_date": week_start,
|
||||||
"schedule_version": selected_version,
|
"schedule_version": selected_version,
|
||||||
|
"schedule_start_date": schedule_start_date,
|
||||||
|
"schedule_start_shift_index": schedule_start_shift_index,
|
||||||
"items": [_serialize_schedule_row(row) for row in rows],
|
"items": [_serialize_schedule_row(row) for row in rows],
|
||||||
"total_scheduled_tests": len(all_rows),
|
"total_scheduled_tests": len(all_rows),
|
||||||
"completion_date": completion_date,
|
"completion_date": completion_date,
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ P1,5,No,No,R3,P2PRXBE008,5G,LPI,T1B,161,80,-67,UL,STA63,,,,,,,,,,,,,85,P2P Only,
|
|||||||
P1,5,No,No,R3,P2PRXBE009,5G,LPI,T1C,161,80,-45,UL,STA63,,,,,,,,,,,,,85,P2P Only,,
|
P1,5,No,No,R3,P2PRXBE009,5G,LPI,T1C,161,80,-45,UL,STA63,,,,,,,,,,,,,85,P2P Only,,
|
||||||
P2,6,Yes,Yes,R3,COERXBE003,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,
|
P2,6,Yes,Yes,R3,COERXBE003,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,
|
||||||
P2,7,Yes,Yes,R3,COERXBE004,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,
|
P2,7,Yes,Yes,R3,COERXBE004,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,
|
||||||
|
P2,9,Yes,Yes,R3,COERXBE006,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,133,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
P1,10,No,Yes,R3,P2PRXBE010,5G,LPI,T1F,100,160,-76,UL,STA63,,,,,,,,,,,,,85,COE Test Case,,
|
P1,10,No,Yes,R3,P2PRXBE010,5G,LPI,T1F,100,160,-76,UL,STA63,,,,,,,,,,,,,85,COE Test Case,,
|
||||||
P1,10,No,No,R3,P2PRXBE011,5G,LPI,T1B,100,160,-67,UL,STA63,,,,,,,,,,,,,85,P2P Only,,
|
P1,10,No,No,R3,P2PRXBE011,5G,LPI,T1B,100,160,-67,UL,STA63,,,,,,,,,,,,,85,P2P Only,,
|
||||||
P1,10,No,No,R3,P2PRXBE012,5G,LPI,T1C,100,160,-45,UL,STA63,,,,,,,,,,,,,85,P2P Only,,
|
P1,10,No,No,R3,P2PRXBE012,5G,LPI,T1C,100,160,-45,UL,STA63,,,,,,,,,,,,,85,P2P Only,,
|
||||||
|
P2,12,Yes,Yes,R3,COERXBE008,5G,LPI,T1F,100,160,-76,UL,STA63,T2A,133,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
P1,13,No,Yes,R1,P2PRXBE013,6G,LPI,,,,,,,T2A,5,160,-79,UL,STA63,,,,,,,85,COE Test Case,Weekend evening CGW452,Weekend evening CGW453
|
P1,13,No,Yes,R1,P2PRXBE013,6G,LPI,,,,,,,T2A,5,160,-79,UL,STA63,,,,,,,85,COE Test Case,Weekend evening CGW452,Weekend evening CGW453
|
||||||
P1,13,No,No,R1,P2PRXBE014,6G,LPI,,,,,,,T1B,5,160,-70,UL,STA63,,,,,,,85,P2P Only,Day 2,Day 2
|
P1,13,No,No,R1,P2PRXBE014,6G,LPI,,,,,,,T1B,5,160,-70,UL,STA63,,,,,,,85,P2P Only,Day 2,Day 2
|
||||||
P1,13,No,No,R1,P2PRXBE015,6G,LPI,,,,,,,T1C,5,160,-45,UL,STA63,,,,,,,85,P2P Only,,
|
P1,13,No,No,R1,P2PRXBE015,6G,LPI,,,,,,,T1C,5,160,-45,UL,STA63,,,,,,,85,P2P Only,,
|
||||||
@@ -27,6 +29,10 @@ P1,17,No,Yes,R1,P2PRXBE019,6G,LPI,,,,,,,T2A,197,160,-79,UL,STA63,,,,,,,85,COE Te
|
|||||||
P1,17,No,No,R1,P2PRXBE020,6G,LPI,,,,,,,T1B,197,160,-70,UL,STA63,,,,,,,85,P2P Only,Day 2,Day 2
|
P1,17,No,No,R1,P2PRXBE020,6G,LPI,,,,,,,T1B,197,160,-70,UL,STA63,,,,,,,85,P2P Only,Day 2,Day 2
|
||||||
P1,17,No,No,R1,P2PRXBE021,6G,LPI,,,,,,,T1C,197,160,-45,UL,STA63,,,,,,,85,P2P Only,,
|
P1,17,No,No,R1,P2PRXBE021,6G,LPI,,,,,,,T1C,197,160,-45,UL,STA63,,,,,,,85,P2P Only,,
|
||||||
P2,18,Yes,Yes,R1,COERXBE011,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,197,160,-79,UL,STA63,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,Weekend evening CGW453
|
P2,18,Yes,Yes,R1,COERXBE011,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,197,160,-79,UL,STA63,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,Weekend evening CGW453
|
||||||
|
P1,21,No,Yes,R1,P2PRXBE025,6G,LPI,,,,,,,T2A,133,320,-79,UL,STA63,,,,,,,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
|
P1,21,No,No,R1,P2PRXBE026,6G,LPI,,,,,,,T1B,133,320,-70,UL,STA63,,,,,,,0,P2P Only,320BW does not support on device,320BW does not support on device
|
||||||
|
P1,21,No,No,R1,P2PRXBE027,6G,LPI,,,,,,,T1C,133,320,-45,UL,STA63,,,,,,,0,P2P Only,320BW does not support on device,320BW does not support on device
|
||||||
|
P2,22,Yes,Yes,R1,COERXBE013,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,320,-79,UL,STA63,T3E,6,20,-70,DL,STA4,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
P1,23,No,Yes,R2,P2PRXBE028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA63,85,COE Test Case,,
|
P1,23,No,Yes,R2,P2PRXBE028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA63,85,COE Test Case,,
|
||||||
P1,23,No,No,R2,P2PRXBE029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA63,85,P2P Only,,
|
P1,23,No,No,R2,P2PRXBE029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA63,85,P2P Only,,
|
||||||
P1,23,No,No,R2,P2PRXBE030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA63,85,P2P Only,,
|
P1,23,No,No,R2,P2PRXBE030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA63,85,P2P Only,,
|
||||||
@@ -55,6 +61,7 @@ P2,7,Yes,Yes,R3,COETXBE004,5G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,160,-79,DL,STA
|
|||||||
P1,8,No,Yes,R3,P2PTXBE010,5G,LPI,T1F,100,160,-76,DL,STA63,,,,,,,,,,,,,85,COE Test Case,,
|
P1,8,No,Yes,R3,P2PTXBE010,5G,LPI,T1F,100,160,-76,DL,STA63,,,,,,,,,,,,,85,COE Test Case,,
|
||||||
P1,8,No,No,R3,P2PTXBE011,5G,LPI,T1B,100,160,-67,DL,STA63,,,,,,,,,,,,,85,P2P Only,,
|
P1,8,No,No,R3,P2PTXBE011,5G,LPI,T1B,100,160,-67,DL,STA63,,,,,,,,,,,,,85,P2P Only,,
|
||||||
P1,8,No,No,R3,P2PTXBE012,5G,LPI,T1C,100,160,-45,DL,STA63,,,,,,,,,,,,,85,P2P Only,,
|
P1,8,No,No,R3,P2PTXBE012,5G,LPI,T1C,100,160,-45,DL,STA63,,,,,,,,,,,,,85,P2P Only,,
|
||||||
|
P2,10,Yes,Yes,R3,COETXBE006,5G,LPI,T1F,100,160,-76,DL,STA63,T2A,133,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
P1,11,No,Yes,R1,P2PTXBE013,6G,LPI,T2A,5,160,-79,DL,STA63,,,,,,,,,,,,,85,COE Test Case,Weekend evening CGW452,Weekend evening CGW453
|
P1,11,No,Yes,R1,P2PTXBE013,6G,LPI,T2A,5,160,-79,DL,STA63,,,,,,,,,,,,,85,COE Test Case,Weekend evening CGW452,Weekend evening CGW453
|
||||||
P1,11,No,No,R1,P2PTXBE014,6G,LPI,T1B,5,160,-70,DL,STA63,,,,,,,,,,,,,85,P2P Only,Day 2,Day 2
|
P1,11,No,No,R1,P2PTXBE014,6G,LPI,T1B,5,160,-70,DL,STA63,,,,,,,,,,,,,85,P2P Only,Day 2,Day 2
|
||||||
P1,11,No,No,R1,P2PTXBE015,6G,LPI,T1C,5,160,-45,DL,STA63,,,,,,,,,,,,,85,P2P Only,Day 1,Day 1
|
P1,11,No,No,R1,P2PTXBE015,6G,LPI,T1C,5,160,-45,DL,STA63,,,,,,,,,,,,,85,P2P Only,Day 1,Day 1
|
||||||
@@ -67,6 +74,10 @@ P1,15,No,Yes,R1,P2PTXBE019,6G,LPI,T2A,197,160,-79,DL,STA63,,,,,,,,,,,,,85,COE Te
|
|||||||
P1,15,No,No,R1,P2PTXBE020,6G,LPI,T1B,197,160,-70,DL,STA63,,,,,,,,,,,,,85,P2P Only,Day 2,Day 2
|
P1,15,No,No,R1,P2PTXBE020,6G,LPI,T1B,197,160,-70,DL,STA63,,,,,,,,,,,,,85,P2P Only,Day 2,Day 2
|
||||||
P1,15,No,No,R1,P2PTXBE021,6G,LPI,T1C,197,160,-45,DL,STA63,,,,,,,,,,,,,85,P2P Only,Day 1,Day 1
|
P1,15,No,No,R1,P2PTXBE021,6G,LPI,T1C,197,160,-45,DL,STA63,,,,,,,,,,,,,85,P2P Only,Day 1,Day 1
|
||||||
P2,16,Yes,Yes,R1,COETXBE009,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,197,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,Weekend evening CGW453
|
P2,16,Yes,Yes,R1,COETXBE009,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,197,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,Weekend evening CGW453
|
||||||
|
P1,19,No,Yes,R1,P2PTXBE025,6G,LPI,T2A,133,320,-79,DL,STA63,,,,,,,,,,,,,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
|
P1,19,No,No,R1,P2PTXBE026,6G,LPI,T1B,133,320,-70,DL,STA63,,,,,,,,,,,,,0,P2P Only,320BW does not support on device,320BW does not support on device
|
||||||
|
P1,19,No,No,R1,P2PTXBE027,6G,LPI,T1C,133,320,-45,DL,STA63,,,,,,,,,,,,,0,P2P Only,320BW does not support on device,320BW does not support on device
|
||||||
|
P2,20,Yes,Yes,R1,COETXBE011,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
P1,21,No,Yes,R2,P2PTXBE028,2G,LPI,T3E,1,20,-70,UL,STA63,,,,,,,,,,,,,85,COE Test Case,,
|
P1,21,No,Yes,R2,P2PTXBE028,2G,LPI,T3E,1,20,-70,UL,STA63,,,,,,,,,,,,,85,COE Test Case,,
|
||||||
P1,21,No,No,R2,P2PTXBE029,2G,LPI,T1D,1,20,-60,UL,STA63,,,,,,,,,,,,,85,P2P Only,Day 4,Day 4
|
P1,21,No,No,R2,P2PTXBE029,2G,LPI,T1D,1,20,-60,UL,STA63,,,,,,,,,,,,,85,P2P Only,Day 4,Day 4
|
||||||
P1,21,No,No,R2,P2PTXBE030,2G,LPI,T1C,1,20,-45,UL,STA63,,,,,,,,,,,,,85,P2P Only,,
|
P1,21,No,No,R2,P2PTXBE030,2G,LPI,T1C,1,20,-45,UL,STA63,,,,,,,,,,,,,85,P2P Only,,
|
||||||
@@ -92,9 +103,11 @@ P1,5,No,No,R1,P2PRXAX008,5G,LPI,T1B,161,80,-67,UL,STA56,,,,,,,,,,,,,85,P2P Only,
|
|||||||
P1,5,No,No,R1,P2PRXAX009,5G,LPI,T1C,161,80,-45,UL,STA56,,,,,,,,,,,,,85,P2P Only,Day 2,Day 2
|
P1,5,No,No,R1,P2PRXAX009,5G,LPI,T1C,161,80,-45,UL,STA56,,,,,,,,,,,,,85,P2P Only,Day 2,Day 2
|
||||||
P2,6,Yes,Yes,R1,COERXAX003,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,Weekend evening CGW453
|
P2,6,Yes,Yes,R1,COERXAX003,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,Weekend evening CGW453
|
||||||
P2,7,Yes,Yes,R1,COERXAX004,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,Weekend evening CGW453
|
P2,7,Yes,Yes,R1,COERXAX004,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,Weekend evening CGW453
|
||||||
|
P2,9,Yes,Yes,R1,COERXAX006,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
P1,10,No,Yes,R1,P2PRXAX010,5G,LPI,T1F,100,160,-76,UL,STA56,,,,,,,,,,,,,85,COE Test Case,Weekend evening CGW452,Weekend evening CGW453
|
P1,10,No,Yes,R1,P2PRXAX010,5G,LPI,T1F,100,160,-76,UL,STA56,,,,,,,,,,,,,85,COE Test Case,Weekend evening CGW452,Weekend evening CGW453
|
||||||
P1,10,No,No,R1,P2PRXAX011,5G,LPI,T1B,100,160,-67,UL,STA56,,,,,,,,,,,,,85,P2P Only,Day 1,Day 1
|
P1,10,No,No,R1,P2PRXAX011,5G,LPI,T1B,100,160,-67,UL,STA56,,,,,,,,,,,,,85,P2P Only,Day 1,Day 1
|
||||||
P1,10,No,No,R1,P2PRXAX012,5G,LPI,T1C,100,160,-45,UL,STA56,,,,,,,,,,,,,85,P2P Only,Day 2,Day 2
|
P1,10,No,No,R1,P2PRXAX012,5G,LPI,T1C,100,160,-45,UL,STA56,,,,,,,,,,,,,85,P2P Only,Day 2,Day 2
|
||||||
|
P2,12,Yes,Yes,R1,COERXAX008,5G,LPI,T1F,100,160,-76,UL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
P1,13,No,Yes,R2,P2PRXAX013,6G,LPI,,,,,,,T2A,5,160,-79,UL,STA56,,,,,,,85,COE Test Case,,
|
P1,13,No,Yes,R2,P2PRXAX013,6G,LPI,,,,,,,T2A,5,160,-79,UL,STA56,,,,,,,85,COE Test Case,,
|
||||||
P1,13,No,No,R2,P2PRXAX014,6G,LPI,,,,,,,T1B,5,160,-70,UL,STA56,,,,,,,85,P2P Only,,
|
P1,13,No,No,R2,P2PRXAX014,6G,LPI,,,,,,,T1B,5,160,-70,UL,STA56,,,,,,,85,P2P Only,,
|
||||||
P1,13,No,No,R2,P2PRXAX015,6G,LPI,,,,,,,T1C,5,160,-45,UL,STA56,,,,,,,85,P2P Only,,
|
P1,13,No,No,R2,P2PRXAX015,6G,LPI,,,,,,,T1C,5,160,-45,UL,STA56,,,,,,,85,P2P Only,,
|
||||||
@@ -107,6 +120,10 @@ P1,17,No,Yes,R2,P2PRXAX019,6G,LPI,,,,,,,T2A,197,160,-79,UL,STA56,,,,,,,85,COE Te
|
|||||||
P1,17,No,No,R2,P2PRXAX020,6G,LPI,,,,,,,T1B,197,160,-70,UL,STA56,,,,,,,85,P2P Only,,
|
P1,17,No,No,R2,P2PRXAX020,6G,LPI,,,,,,,T1B,197,160,-70,UL,STA56,,,,,,,85,P2P Only,,
|
||||||
P1,17,No,No,R2,P2PRXAX021,6G,LPI,,,,,,,T1C,197,160,-45,UL,STA56,,,,,,,85,P2P Only,,
|
P1,17,No,No,R2,P2PRXAX021,6G,LPI,,,,,,,T1C,197,160,-45,UL,STA56,,,,,,,85,P2P Only,,
|
||||||
P2,18,Yes,Yes,R2,COERXAX011,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,197,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,
|
P2,18,Yes,Yes,R2,COERXAX011,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,197,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,
|
||||||
|
P1,21,No,Yes,R2,P2PRXAX025,6G,LPI,,,,,,,T2A,133,320,-79,UL,STA56,,,,,,,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
|
P1,21,No,No,R2,P2PRXAX026,6G,LPI,,,,,,,T1B,133,320,-70,UL,STA56,,,,,,,0,P2P Only,320BW does not support on device,320BW does not support on device
|
||||||
|
P1,21,No,No,R2,P2PRXAX027,6G,LPI,,,,,,,T1C,133,320,-45,UL,STA56,,,,,,,0,P2P Only,320BW does not support on device,320BW does not support on device
|
||||||
|
P2,22,Yes,Yes,R2,COERXAX013,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,320,-79,UL,STA56,T3E,6,20,-70,DL,STA4,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
P1,23,No,Yes,R3,P2PRXAX028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA56,85,COE Test Case,,
|
P1,23,No,Yes,R3,P2PRXAX028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA56,85,COE Test Case,,
|
||||||
P1,23,No,No,R3,P2PRXAX029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA56,85,P2P Only,,
|
P1,23,No,No,R3,P2PRXAX029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA56,85,P2P Only,,
|
||||||
P1,23,No,No,R3,P2PRXAX030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA56,85,P2P Only,,
|
P1,23,No,No,R3,P2PRXAX030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA56,85,P2P Only,,
|
||||||
@@ -135,6 +152,7 @@ P2,7,Yes,Yes,R1,COETXAX004,5G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,160,-79,DL,STA
|
|||||||
P1,8,No,Yes,R1,P2PTXAX010,5G,LPI,T1F,100,160,-76,DL,STA56,,,,,,,,,,,,,85,COE Test Case,Weekend evening CGW452,Weekend evening CGW453
|
P1,8,No,Yes,R1,P2PTXAX010,5G,LPI,T1F,100,160,-76,DL,STA56,,,,,,,,,,,,,85,COE Test Case,Weekend evening CGW452,Weekend evening CGW453
|
||||||
P1,8,No,No,R1,P2PTXAX011,5G,LPI,T1B,100,160,-67,DL,STA56,,,,,,,,,,,,,85,P2P Only,Day 1,Day 1
|
P1,8,No,No,R1,P2PTXAX011,5G,LPI,T1B,100,160,-67,DL,STA56,,,,,,,,,,,,,85,P2P Only,Day 1,Day 1
|
||||||
P1,8,No,No,R1,P2PTXAX012,5G,LPI,T1C,100,160,-45,DL,STA56,,,,,,,,,,,,,85,P2P Only,Day 3,Day 3
|
P1,8,No,No,R1,P2PTXAX012,5G,LPI,T1C,100,160,-45,DL,STA56,,,,,,,,,,,,,85,P2P Only,Day 3,Day 3
|
||||||
|
P2,10,Yes,Yes,R1,COETXAX006,5G,LPI,T1F,100,160,-76,DL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
P1,11,No,Yes,R2,P2PTXAX013,6G,LPI,,,,,,,T2A,5,160,-79,DL,STA56,,,,,,,85,COE Test Case,,
|
P1,11,No,Yes,R2,P2PTXAX013,6G,LPI,,,,,,,T2A,5,160,-79,DL,STA56,,,,,,,85,COE Test Case,,
|
||||||
P1,11,No,No,R2,P2PTXAX014,6G,LPI,,,,,,,T1B,5,160,-70,DL,STA56,,,,,,,85,P2P Only,,
|
P1,11,No,No,R2,P2PTXAX014,6G,LPI,,,,,,,T1B,5,160,-70,DL,STA56,,,,,,,85,P2P Only,,
|
||||||
P1,11,No,No,R2,P2PTXAX015,6G,LPI,,,,,,,T1C,5,160,-45,DL,STA56,,,,,,,85,P2P Only,,
|
P1,11,No,No,R2,P2PTXAX015,6G,LPI,,,,,,,T1C,5,160,-45,DL,STA56,,,,,,,85,P2P Only,,
|
||||||
@@ -147,6 +165,10 @@ P1,15,No,Yes,R2,P2PTXAX019,6G,LPI,,,,,,,T2A,197,160,-79,DL,STA56,,,,,,,85,COE Te
|
|||||||
P1,15,No,No,R2,P2PTXAX020,6G,LPI,,,,,,,T1B,197,160,-70,DL,STA56,,,,,,,85,P2P Only,,
|
P1,15,No,No,R2,P2PTXAX020,6G,LPI,,,,,,,T1B,197,160,-70,DL,STA56,,,,,,,85,P2P Only,,
|
||||||
P1,15,No,No,R2,P2PTXAX021,6G,LPI,,,,,,,T1C,197,160,-45,DL,STA56,,,,,,,85,P2P Only,,
|
P1,15,No,No,R2,P2PTXAX021,6G,LPI,,,,,,,T1C,197,160,-45,DL,STA56,,,,,,,85,P2P Only,,
|
||||||
P2,16,Yes,Yes,R2,COETXAX009,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,197,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,
|
P2,16,Yes,Yes,R2,COETXAX009,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,197,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4,144,COE Test Case,COE test case. No plan to run,
|
||||||
|
P1,19,No,Yes,R2,P2PTXAX025,6G,LPI,,,,,,,T2A,133,320,-79,DL,STA56,,,,,,,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
|
P1,19,No,No,R2,P2PTXAX026,6G,LPI,,,,,,,T1B,133,320,-70,DL,STA56,,,,,,,0,P2P Only,320BW does not support on device,320BW does not support on device
|
||||||
|
P1,19,No,No,R2,P2PTXAX027,6G,LPI,,,,,,,T1C,133,320,-45,DL,STA56,,,,,,,0,P2P Only,320BW does not support on device,320BW does not support on device
|
||||||
|
P2,20,Yes,Yes,R2,COETXAX011,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,320,-79,DL,STA56,T3E,6,20,-70,DL,STA4,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
P1,21,No,Yes,R3,P2PTXAX028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA56,85,COE Test Case,,
|
P1,21,No,Yes,R3,P2PTXAX028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA56,85,COE Test Case,,
|
||||||
P1,21,No,No,R3,P2PTXAX029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA56,85,P2P Only,,
|
P1,21,No,No,R3,P2PTXAX029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA56,85,P2P Only,,
|
||||||
P1,21,No,No,R3,P2PTXAX030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA56,85,P2P Only,,
|
P1,21,No,No,R3,P2PTXAX030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA56,85,P2P Only,,
|
||||||
@@ -204,3 +226,4 @@ P2,17,Yes,Yes,R1,COETXAC004,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,133,160,OFF,DL,STA
|
|||||||
P1,18,No,Yes,R1,P2PTXAC016,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,DL,STA4,85,COE Test Case,Weekend evening CGW452,
|
P1,18,No,Yes,R1,P2PTXAC016,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,DL,STA4,85,COE Test Case,Weekend evening CGW452,
|
||||||
P1,18,No,No,R1,P2PTXAC017,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,DL,STA4,85,P2P Only,Day 3,Day 3
|
P1,18,No,No,R1,P2PTXAC017,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,DL,STA4,85,P2P Only,Day 3,Day 3
|
||||||
P1,18,No,No,R1,P2PTXAC018,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,DL,STA4,85,P2P Only,Day 4,Day 4
|
P1,18,No,No,R1,P2PTXAC018,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,DL,STA4,85,P2P Only,Day 4,Day 4
|
||||||
|
P2,19,Yes,Yes,R1,COETXAC005,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,5,320,OFF,DL,STA63,T3E,11,20,-70,DL,STA4,0,COE Test Case,320BW does not support on device,320BW does not support on device
|
||||||
|
+120
-87
@@ -19,6 +19,7 @@ REF = os.getenv("REF", "CGW452").strip()
|
|||||||
# Device names for the test database (use hardware device names)
|
# Device names for the test database (use hardware device names)
|
||||||
DEVICE_DUT = DUT
|
DEVICE_DUT = DUT
|
||||||
DEVICE_REF = REF
|
DEVICE_REF = REF
|
||||||
|
MAX_SCHEDULE_VERSIONS = 50
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class TestRecord:
|
class TestRecord:
|
||||||
@@ -139,6 +140,20 @@ def init_db(db_path: str | Path = DB_PATH) -> None:
|
|||||||
_ensure_column(conn, "tests", "excluded", "INTEGER NOT NULL DEFAULT 0")
|
_ensure_column(conn, "tests", "excluded", "INTEGER NOT NULL DEFAULT 0")
|
||||||
_ensure_column(conn, "tests", "throttled", "INTEGER NOT NULL DEFAULT 0")
|
_ensure_column(conn, "tests", "throttled", "INTEGER NOT NULL DEFAULT 0")
|
||||||
_ensure_column(conn, "tests", "station_testpoint_map", "TEXT")
|
_ensure_column(conn, "tests", "station_testpoint_map", "TEXT")
|
||||||
|
_ensure_column(conn, "schedules", "status_snapshot", "TEXT")
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE schedules
|
||||||
|
SET status_snapshot = COALESCE((
|
||||||
|
SELECT t.status
|
||||||
|
FROM tests t
|
||||||
|
WHERE t.test_id = schedules.test_id
|
||||||
|
AND t.device = schedules.device
|
||||||
|
), 'pending')
|
||||||
|
WHERE status_snapshot IS NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
prune_schedule_versions(conn=conn)
|
||||||
|
|
||||||
# Seed runtime defaults for schedule estimation.
|
# Seed runtime defaults for schedule estimation.
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -454,7 +469,7 @@ def list_holidays(db_path: str | Path = DB_PATH) -> set[str]:
|
|||||||
|
|
||||||
|
|
||||||
def create_schedule_version(
|
def create_schedule_version(
|
||||||
entries: list[tuple[str, str, str, int, int]],
|
entries: list[tuple[str, str, str, int, int, str]],
|
||||||
db_path: str | Path = DB_PATH,
|
db_path: str | Path = DB_PATH,
|
||||||
) -> int:
|
) -> int:
|
||||||
with get_connection(db_path) as conn:
|
with get_connection(db_path) as conn:
|
||||||
@@ -464,18 +479,102 @@ def create_schedule_version(
|
|||||||
if entries:
|
if entries:
|
||||||
conn.executemany(
|
conn.executemany(
|
||||||
"""
|
"""
|
||||||
INSERT INTO schedules(schedule_version, test_id, device, scheduled_date, shift_index, sequence_in_shift)
|
INSERT INTO schedules(
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
schedule_version,
|
||||||
|
test_id,
|
||||||
|
device,
|
||||||
|
scheduled_date,
|
||||||
|
shift_index,
|
||||||
|
sequence_in_shift,
|
||||||
|
status_snapshot
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
[
|
[
|
||||||
(next_version, test_id, device, scheduled_date, shift_index, sequence)
|
(next_version, test_id, device, scheduled_date, shift_index, sequence, status_snapshot)
|
||||||
for test_id, device, scheduled_date, shift_index, sequence in entries
|
for test_id, device, scheduled_date, shift_index, sequence, status_snapshot in entries
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
prune_schedule_versions(conn=conn)
|
||||||
|
|
||||||
return next_version
|
return next_version
|
||||||
|
|
||||||
|
|
||||||
|
def prune_schedule_versions(
|
||||||
|
max_versions: int = MAX_SCHEDULE_VERSIONS,
|
||||||
|
db_path: str | Path = DB_PATH,
|
||||||
|
conn: sqlite3.Connection | None = None,
|
||||||
|
) -> int:
|
||||||
|
if max_versions <= 0:
|
||||||
|
raise ValueError("max_versions must be positive")
|
||||||
|
|
||||||
|
def _prune(active_conn: sqlite3.Connection) -> int:
|
||||||
|
cursor = active_conn.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM schedules
|
||||||
|
WHERE schedule_version IN (
|
||||||
|
SELECT schedule_version
|
||||||
|
FROM (
|
||||||
|
SELECT schedule_version
|
||||||
|
FROM schedules
|
||||||
|
GROUP BY schedule_version
|
||||||
|
ORDER BY schedule_version DESC
|
||||||
|
LIMIT -1 OFFSET ?
|
||||||
|
)
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
(max_versions,),
|
||||||
|
)
|
||||||
|
return cursor.rowcount
|
||||||
|
|
||||||
|
if conn is not None:
|
||||||
|
return _prune(conn)
|
||||||
|
|
||||||
|
with get_connection(db_path) as active_conn:
|
||||||
|
return _prune(active_conn)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_requested_schedule_version(
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
version: int | None,
|
||||||
|
) -> tuple[int | None, int | None]:
|
||||||
|
latest_row = conn.execute("SELECT MAX(schedule_version) AS latest FROM schedules").fetchone()
|
||||||
|
latest = latest_row["latest"]
|
||||||
|
latest_version = int(latest) if latest is not None else None
|
||||||
|
|
||||||
|
if version is not None:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT 1 AS exists_row FROM schedules WHERE schedule_version = ? LIMIT 1",
|
||||||
|
(version,),
|
||||||
|
).fetchone()
|
||||||
|
return (int(version) if row else None, latest_version)
|
||||||
|
|
||||||
|
return latest_version, latest_version
|
||||||
|
|
||||||
|
|
||||||
|
def _hydrate_schedule_rows(rows: list[sqlite3.Row]) -> list[ScheduleRow]:
|
||||||
|
result: list[ScheduleRow] = []
|
||||||
|
for row in rows:
|
||||||
|
result.append(
|
||||||
|
ScheduleRow(
|
||||||
|
test_id=row["test_id"],
|
||||||
|
device=row["device"],
|
||||||
|
scheduled_date=row["scheduled_date"],
|
||||||
|
shift_index=int(row["shift_index"]),
|
||||||
|
sequence_in_shift=int(row["sequence_in_shift"]),
|
||||||
|
test_type=row["test_type"],
|
||||||
|
rotation=row["rotation"],
|
||||||
|
config=json.loads(row["config_json"] or "{}"),
|
||||||
|
status=row["status"],
|
||||||
|
priority=int(row["priority"]),
|
||||||
|
estimated_minutes=int(row["estimated_minutes"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_schedule_versions(db_path: str | Path = DB_PATH) -> list[dict[str, Any]]:
|
def get_schedule_versions(db_path: str | Path = DB_PATH) -> list[dict[str, Any]]:
|
||||||
with get_connection(db_path) as conn:
|
with get_connection(db_path) as conn:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
@@ -505,16 +604,8 @@ def resolve_schedule_version(
|
|||||||
db_path: str | Path = DB_PATH,
|
db_path: str | Path = DB_PATH,
|
||||||
) -> int | None:
|
) -> int | None:
|
||||||
with get_connection(db_path) as conn:
|
with get_connection(db_path) as conn:
|
||||||
if version is not None:
|
selected_version, _latest_version = _resolve_requested_schedule_version(conn, version)
|
||||||
row = conn.execute(
|
return selected_version
|
||||||
"SELECT 1 FROM schedules WHERE schedule_version = ? LIMIT 1",
|
|
||||||
(version,),
|
|
||||||
).fetchone()
|
|
||||||
return int(version) if row else None
|
|
||||||
|
|
||||||
latest_row = conn.execute("SELECT MAX(schedule_version) AS latest FROM schedules").fetchone()
|
|
||||||
latest = latest_row["latest"]
|
|
||||||
return int(latest) if latest is not None else None
|
|
||||||
|
|
||||||
|
|
||||||
def get_schedule_week(
|
def get_schedule_week(
|
||||||
@@ -523,26 +614,15 @@ def get_schedule_week(
|
|||||||
db_path: str | Path = DB_PATH,
|
db_path: str | Path = DB_PATH,
|
||||||
) -> list[ScheduleRow]:
|
) -> list[ScheduleRow]:
|
||||||
with get_connection(db_path) as conn:
|
with get_connection(db_path) as conn:
|
||||||
if version is not None:
|
selected_version, latest_version = _resolve_requested_schedule_version(conn, version)
|
||||||
version_row = conn.execute(
|
|
||||||
"SELECT 1 AS exists_row FROM schedules WHERE schedule_version = ? LIMIT 1",
|
|
||||||
(version,),
|
|
||||||
).fetchone()
|
|
||||||
if version_row is None:
|
|
||||||
return []
|
|
||||||
selected_version = int(version)
|
|
||||||
else:
|
|
||||||
latest_row = conn.execute("SELECT MAX(schedule_version) AS latest FROM schedules").fetchone()
|
|
||||||
latest = latest_row["latest"]
|
|
||||||
if latest is None:
|
|
||||||
return []
|
|
||||||
selected_version = int(latest)
|
|
||||||
|
|
||||||
if selected_version is None:
|
if selected_version is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
use_live_status = latest_version is not None and selected_version == latest_version
|
||||||
|
status_sql = "COALESCE(t.status, s.status_snapshot, 'pending')" if use_live_status else "COALESCE(s.status_snapshot, 'pending')"
|
||||||
|
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
s.test_id,
|
s.test_id,
|
||||||
s.device,
|
s.device,
|
||||||
@@ -552,7 +632,7 @@ def get_schedule_week(
|
|||||||
t.test_type,
|
t.test_type,
|
||||||
t.rotation,
|
t.rotation,
|
||||||
t.config_json,
|
t.config_json,
|
||||||
t.status,
|
{status_sql} AS status,
|
||||||
t.priority,
|
t.priority,
|
||||||
t.estimated_minutes
|
t.estimated_minutes
|
||||||
FROM schedules s
|
FROM schedules s
|
||||||
@@ -586,25 +666,7 @@ def get_schedule_week(
|
|||||||
),
|
),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
result: list[ScheduleRow] = []
|
return _hydrate_schedule_rows(rows)
|
||||||
for row in rows:
|
|
||||||
result.append(
|
|
||||||
ScheduleRow(
|
|
||||||
test_id=row["test_id"],
|
|
||||||
device=row["device"],
|
|
||||||
scheduled_date=row["scheduled_date"],
|
|
||||||
shift_index=int(row["shift_index"]),
|
|
||||||
sequence_in_shift=int(row["sequence_in_shift"]),
|
|
||||||
test_type=row["test_type"],
|
|
||||||
rotation=row["rotation"],
|
|
||||||
config=json.loads(row["config_json"] or "{}"),
|
|
||||||
status=row["status"],
|
|
||||||
priority=int(row["priority"]),
|
|
||||||
estimated_minutes=int(row["estimated_minutes"]),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def get_schedule_rows(
|
def get_schedule_rows(
|
||||||
@@ -612,26 +674,15 @@ def get_schedule_rows(
|
|||||||
db_path: str | Path = DB_PATH,
|
db_path: str | Path = DB_PATH,
|
||||||
) -> list[ScheduleRow]:
|
) -> list[ScheduleRow]:
|
||||||
with get_connection(db_path) as conn:
|
with get_connection(db_path) as conn:
|
||||||
if version is not None:
|
selected_version, latest_version = _resolve_requested_schedule_version(conn, version)
|
||||||
version_row = conn.execute(
|
|
||||||
"SELECT 1 AS exists_row FROM schedules WHERE schedule_version = ? LIMIT 1",
|
|
||||||
(version,),
|
|
||||||
).fetchone()
|
|
||||||
if version_row is None:
|
|
||||||
return []
|
|
||||||
selected_version = int(version)
|
|
||||||
else:
|
|
||||||
latest_row = conn.execute("SELECT MAX(schedule_version) AS latest FROM schedules").fetchone()
|
|
||||||
latest = latest_row["latest"]
|
|
||||||
if latest is None:
|
|
||||||
return []
|
|
||||||
selected_version = int(latest)
|
|
||||||
|
|
||||||
if selected_version is None:
|
if selected_version is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
use_live_status = latest_version is not None and selected_version == latest_version
|
||||||
|
status_sql = "COALESCE(t.status, s.status_snapshot, 'pending')" if use_live_status else "COALESCE(s.status_snapshot, 'pending')"
|
||||||
|
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT
|
SELECT
|
||||||
s.test_id,
|
s.test_id,
|
||||||
s.device,
|
s.device,
|
||||||
@@ -641,7 +692,7 @@ def get_schedule_rows(
|
|||||||
t.test_type,
|
t.test_type,
|
||||||
t.rotation,
|
t.rotation,
|
||||||
t.config_json,
|
t.config_json,
|
||||||
t.status,
|
{status_sql} AS status,
|
||||||
t.priority,
|
t.priority,
|
||||||
t.estimated_minutes
|
t.estimated_minutes
|
||||||
FROM schedules s
|
FROM schedules s
|
||||||
@@ -652,25 +703,7 @@ def get_schedule_rows(
|
|||||||
(selected_version,),
|
(selected_version,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
result: list[ScheduleRow] = []
|
return _hydrate_schedule_rows(rows)
|
||||||
for row in rows:
|
|
||||||
result.append(
|
|
||||||
ScheduleRow(
|
|
||||||
test_id=row["test_id"],
|
|
||||||
device=row["device"],
|
|
||||||
scheduled_date=row["scheduled_date"],
|
|
||||||
shift_index=int(row["shift_index"]),
|
|
||||||
sequence_in_shift=int(row["sequence_in_shift"]),
|
|
||||||
test_type=row["test_type"],
|
|
||||||
rotation=row["rotation"],
|
|
||||||
config=json.loads(row["config_json"] or "{}"),
|
|
||||||
status=row["status"],
|
|
||||||
priority=int(row["priority"]),
|
|
||||||
estimated_minutes=int(row["estimated_minutes"]),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def reset_completed_to_pending(db_path: str | Path = DB_PATH) -> int:
|
def reset_completed_to_pending(db_path: str | Path = DB_PATH) -> int:
|
||||||
"""Reset all 'completed' tests back to 'pending'. Used before a full directory rescan."""
|
"""Reset all 'completed' tests back to 'pending'. Used before a full directory rescan."""
|
||||||
|
|||||||
+113
-1
@@ -119,6 +119,87 @@ function toTonightConfigRows(scheduleData) {
|
|||||||
.sort((a, b) => a.sta.localeCompare(b.sta))
|
.sort((a, b) => a.sta.localeCompare(b.sta))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SHIFT_KEYS = ['shift1', 'shift2', 'shift3']
|
||||||
|
const SHIFT_INDEX_BY_KEY = {
|
||||||
|
shift1: 1,
|
||||||
|
shift2: 2,
|
||||||
|
shift3: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyScheduleDay() {
|
||||||
|
return { shift1: [], shift2: [], shift3: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getShiftTests(scheduleData, dateKey, shiftKey) {
|
||||||
|
return scheduleData[dateKey]?.[shiftKey] ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOnOrAfterScheduleStart(dateKey, shiftKey, scheduleStartShift) {
|
||||||
|
if (!scheduleStartShift?.date || !scheduleStartShift?.shiftIndex) return false
|
||||||
|
|
||||||
|
if (dateKey > scheduleStartShift.date) return true
|
||||||
|
if (dateKey < scheduleStartShift.date) return false
|
||||||
|
|
||||||
|
const currentShiftIndex = SHIFT_INDEX_BY_KEY[shiftKey] ?? 99
|
||||||
|
return currentShiftIndex >= scheduleStartShift.shiftIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectSnapshotShiftKeys(currentScheduleData = {}, previousScheduleData = {}, scheduleStartShift = null) {
|
||||||
|
const snapshotShiftKeys = new Set()
|
||||||
|
const dateKeys = new Set([
|
||||||
|
...Object.keys(currentScheduleData),
|
||||||
|
...Object.keys(previousScheduleData),
|
||||||
|
])
|
||||||
|
|
||||||
|
for (const dateKey of dateKeys) {
|
||||||
|
for (const shiftKey of SHIFT_KEYS) {
|
||||||
|
if (isOnOrAfterScheduleStart(dateKey, shiftKey, scheduleStartShift)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTests = getShiftTests(currentScheduleData, dateKey, shiftKey)
|
||||||
|
const previousTests = getShiftTests(previousScheduleData, dateKey, shiftKey)
|
||||||
|
|
||||||
|
// Show historical snapshot only when this shift disappeared in the latest schedule.
|
||||||
|
if (currentTests.length === 0 && previousTests.length > 0) {
|
||||||
|
snapshotShiftKeys.add(`${dateKey}::${shiftKey}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshotShiftKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
function markHistoricalSnapshot(tests = []) {
|
||||||
|
return tests.map((test) => ({
|
||||||
|
...test,
|
||||||
|
isPreviousVersionSnapshot: true,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCalendarScheduleData(currentScheduleData = {}, previousScheduleData = {}, snapshotShiftKeys = new Set()) {
|
||||||
|
const calendarScheduleData = {}
|
||||||
|
const dateKeys = new Set([
|
||||||
|
...Object.keys(currentScheduleData),
|
||||||
|
...Object.keys(previousScheduleData),
|
||||||
|
])
|
||||||
|
|
||||||
|
for (const dateKey of dateKeys) {
|
||||||
|
const mergedDay = createEmptyScheduleDay()
|
||||||
|
|
||||||
|
for (const shiftKey of SHIFT_KEYS) {
|
||||||
|
const shiftSnapshotKey = `${dateKey}::${shiftKey}`
|
||||||
|
mergedDay[shiftKey] = snapshotShiftKeys.has(shiftSnapshotKey)
|
||||||
|
? markHistoricalSnapshot(getShiftTests(previousScheduleData, dateKey, shiftKey))
|
||||||
|
: getShiftTests(currentScheduleData, dateKey, shiftKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
calendarScheduleData[dateKey] = mergedDay
|
||||||
|
}
|
||||||
|
|
||||||
|
return calendarScheduleData
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_SETTINGS = {
|
const DEFAULT_SETTINGS = {
|
||||||
p2pCoeCsvPath: '',
|
p2pCoeCsvPath: '',
|
||||||
p3pCsvPath: '',
|
p3pCsvPath: '',
|
||||||
@@ -171,6 +252,8 @@ export default function App() {
|
|||||||
const [startDateOverride, setStartDateOverride] = useState('')
|
const [startDateOverride, setStartDateOverride] = useState('')
|
||||||
const [failedTests, setFailedTests] = useState([])
|
const [failedTests, setFailedTests] = useState([])
|
||||||
const [scheduleData, setScheduleData] = useState({})
|
const [scheduleData, setScheduleData] = useState({})
|
||||||
|
const [previousScheduleData, setPreviousScheduleData] = useState({})
|
||||||
|
const [scheduleStartShift, setScheduleStartShift] = useState(null)
|
||||||
const [scheduleWindows, setScheduleWindows] = useState([])
|
const [scheduleWindows, setScheduleWindows] = useState([])
|
||||||
const [selectedWindowId, setSelectedWindowId] = useState(null)
|
const [selectedWindowId, setSelectedWindowId] = useState(null)
|
||||||
const [completionDate, setCompletionDate] = useState(null)
|
const [completionDate, setCompletionDate] = useState(null)
|
||||||
@@ -178,6 +261,14 @@ export default function App() {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const tonightConfigRows = useMemo(() => toTonightConfigRows(scheduleData), [scheduleData])
|
const tonightConfigRows = useMemo(() => toTonightConfigRows(scheduleData), [scheduleData])
|
||||||
|
const snapshotShiftKeys = useMemo(
|
||||||
|
() => collectSnapshotShiftKeys(scheduleData, previousScheduleData, scheduleStartShift),
|
||||||
|
[scheduleData, previousScheduleData, scheduleStartShift],
|
||||||
|
)
|
||||||
|
const calendarScheduleData = useMemo(
|
||||||
|
() => buildCalendarScheduleData(scheduleData, previousScheduleData, snapshotShiftKeys),
|
||||||
|
[scheduleData, previousScheduleData, snapshotShiftKeys],
|
||||||
|
)
|
||||||
const windowLookup = useMemo(() => {
|
const windowLookup = useMemo(() => {
|
||||||
const lookup = new Map()
|
const lookup = new Map()
|
||||||
|
|
||||||
@@ -205,7 +296,27 @@ export default function App() {
|
|||||||
const fetchSchedule = useCallback(async (start) => {
|
const fetchSchedule = useCallback(async (start) => {
|
||||||
try {
|
try {
|
||||||
const data = await api.getScheduleWeek(toKey(start))
|
const data = await api.getScheduleWeek(toKey(start))
|
||||||
|
let previousVersionScheduleData = {}
|
||||||
|
|
||||||
|
if ((data.schedule_version ?? 0) > 1) {
|
||||||
|
try {
|
||||||
|
const previousData = await api.getScheduleWeek(toKey(start), data.schedule_version - 1)
|
||||||
|
previousVersionScheduleData = groupScheduleItems(previousData.items)
|
||||||
|
} catch (previousError) {
|
||||||
|
console.warn('Failed to fetch previous schedule version:', previousError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setScheduleData(groupScheduleItems(data.items))
|
setScheduleData(groupScheduleItems(data.items))
|
||||||
|
setPreviousScheduleData(previousVersionScheduleData)
|
||||||
|
if (data.schedule_start_date && data.schedule_start_shift_index) {
|
||||||
|
setScheduleStartShift({
|
||||||
|
date: data.schedule_start_date,
|
||||||
|
shiftIndex: data.schedule_start_shift_index,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setScheduleStartShift(null)
|
||||||
|
}
|
||||||
setScheduleWindows(data.windows ?? [])
|
setScheduleWindows(data.windows ?? [])
|
||||||
setCompletionDate(data.completion_date ?? null)
|
setCompletionDate(data.completion_date ?? null)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -386,7 +497,7 @@ export default function App() {
|
|||||||
<main className="relative flex flex-1 gap-4 p-4 overflow-hidden">
|
<main className="relative flex flex-1 gap-4 p-4 overflow-hidden">
|
||||||
<div className="flex-1 min-w-0 flex flex-col">
|
<div className="flex-1 min-w-0 flex flex-col">
|
||||||
<Calendar
|
<Calendar
|
||||||
scheduleData={scheduleData}
|
scheduleData={calendarScheduleData}
|
||||||
daytimeDateKey={null}
|
daytimeDateKey={null}
|
||||||
weekStart={weekStart}
|
weekStart={weekStart}
|
||||||
onWeekChange={setWeekStart}
|
onWeekChange={setWeekStart}
|
||||||
@@ -398,6 +509,7 @@ export default function App() {
|
|||||||
}))
|
}))
|
||||||
}}
|
}}
|
||||||
windowLookup={windowLookup}
|
windowLookup={windowLookup}
|
||||||
|
snapshotShiftKeys={snapshotShiftKeys}
|
||||||
onWindowSelect={handleWindowSelect}
|
onWindowSelect={handleWindowSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+7
-1
@@ -30,7 +30,13 @@ export const api = {
|
|||||||
|
|
||||||
// Schedule
|
// Schedule
|
||||||
compileSchedule: (opts) => request('POST', '/schedule/compile', opts),
|
compileSchedule: (opts) => request('POST', '/schedule/compile', opts),
|
||||||
getScheduleWeek: (start) => request('GET', `/schedule/week?start=${start}`),
|
getScheduleWeek: (start, version = null) => {
|
||||||
|
const params = new URLSearchParams({ start })
|
||||||
|
if (version !== null && version !== undefined) {
|
||||||
|
params.set('version', String(version))
|
||||||
|
}
|
||||||
|
return request('GET', `/schedule/week?${params.toString()}`)
|
||||||
|
},
|
||||||
getRerunTests: () => request('GET', '/tests/rerun'),
|
getRerunTests: () => request('GET', '/tests/rerun'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export default function Calendar({
|
|||||||
dualDeviceWeekendWeekEnabled = false,
|
dualDeviceWeekendWeekEnabled = false,
|
||||||
onDualDeviceWeekendWeekEnabledChange,
|
onDualDeviceWeekendWeekEnabledChange,
|
||||||
windowLookup = new Map(),
|
windowLookup = new Map(),
|
||||||
|
snapshotShiftKeys = new Set(),
|
||||||
onWindowSelect,
|
onWindowSelect,
|
||||||
}) {
|
}) {
|
||||||
const DEFAULT_SHIFT_MIN_ROWS = 3
|
const DEFAULT_SHIFT_MIN_ROWS = 3
|
||||||
@@ -226,6 +227,7 @@ export default function Calendar({
|
|||||||
slotHeights={slotHeights}
|
slotHeights={slotHeights}
|
||||||
showShift2={isWeekend || (daytimeDateKey !== null && key === daytimeDateKey)}
|
showShift2={isWeekend || (daytimeDateKey !== null && key === daytimeDateKey)}
|
||||||
windowLookup={windowLookup}
|
windowLookup={windowLookup}
|
||||||
|
snapshotShiftKeys={snapshotShiftKeys}
|
||||||
onWindowSelect={onWindowSelect}
|
onWindowSelect={onWindowSelect}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default function DayColumn({
|
|||||||
shiftMinRows = { shift2: 1, shift3: 1, nextDayShift1: 1 },
|
shiftMinRows = { shift2: 1, shift3: 1, nextDayShift1: 1 },
|
||||||
slotHeights = { shift2: 0, shift3: 0, nextDayShift1: 0 },
|
slotHeights = { shift2: 0, shift3: 0, nextDayShift1: 0 },
|
||||||
windowLookup = new Map(),
|
windowLookup = new Map(),
|
||||||
|
snapshotShiftKeys = new Set(),
|
||||||
onWindowSelect,
|
onWindowSelect,
|
||||||
}) {
|
}) {
|
||||||
const dayName = DAY_NAMES[date.getDay()]
|
const dayName = DAY_NAMES[date.getDay()]
|
||||||
@@ -20,11 +21,17 @@ export default function DayColumn({
|
|||||||
const dateKey = date.toISOString().slice(0, 10)
|
const dateKey = date.toISOString().slice(0, 10)
|
||||||
const nextDateKey = new Date(date.getTime() + (24 * 60 * 60 * 1000)).toISOString().slice(0, 10)
|
const nextDateKey = new Date(date.getTime() + (24 * 60 * 60 * 1000)).toISOString().slice(0, 10)
|
||||||
|
|
||||||
|
function isSnapshotShift(snapshotDateKey, shiftIndex) {
|
||||||
|
return snapshotShiftKeys.has(`${snapshotDateKey}::shift${shiftIndex}`)
|
||||||
|
}
|
||||||
|
|
||||||
function getWindowForShift(shiftIndex) {
|
function getWindowForShift(shiftIndex) {
|
||||||
|
if (isSnapshotShift(dateKey, shiftIndex)) return null
|
||||||
return windowLookup.get(`${dateKey}::shift${shiftIndex}`) ?? null
|
return windowLookup.get(`${dateKey}::shift${shiftIndex}`) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNextDayShift1Window() {
|
function getNextDayShift1Window() {
|
||||||
|
if (isSnapshotShift(nextDateKey, 1)) return null
|
||||||
return windowLookup.get(`${nextDateKey}::shift1`) ?? null
|
return windowLookup.get(`${nextDateKey}::shift1`) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export default function TestCard({ test }) {
|
|||||||
const normalizedStatus = String(test?.status ?? 'pending').trim().toLowerCase()
|
const normalizedStatus = String(test?.status ?? 'pending').trim().toLowerCase()
|
||||||
const style = STATUS_STYLES[normalizedStatus] ?? STATUS_STYLES.pending
|
const style = STATUS_STYLES[normalizedStatus] ?? STATUS_STYLES.pending
|
||||||
const deviceAccentClass = getDeviceAccentClass(test.device)
|
const deviceAccentClass = getDeviceAccentClass(test.device)
|
||||||
|
const isPreviousVersionSnapshot = Boolean(test?.isPreviousVersionSnapshot)
|
||||||
const cardRef = useRef(null)
|
const cardRef = useRef(null)
|
||||||
const [tooltipPos, setTooltipPos] = useState({ top: '0px', left: '0px' })
|
const [tooltipPos, setTooltipPos] = useState({ top: '0px', left: '0px' })
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
@@ -92,7 +93,7 @@ export default function TestCard({ test }) {
|
|||||||
<div ref={cardRef} className="relative">
|
<div ref={cardRef} className="relative">
|
||||||
<div
|
<div
|
||||||
className={`relative px-1.5 py-0.5 pl-3 rounded border text-xs font-mono truncate cursor-default select-none ${style}`}
|
className={`relative px-1.5 py-0.5 pl-3 rounded border text-xs font-mono truncate cursor-default select-none ${style}`}
|
||||||
style={{ maxWidth: '100%' }}
|
style={{ maxWidth: '100%', opacity: isPreviousVersionSnapshot ? 0.45 : 1 }}
|
||||||
>
|
>
|
||||||
<span className={`absolute inset-y-0 left-0 w-1 rounded-l ${deviceAccentClass}`} aria-hidden="true" />
|
<span className={`absolute inset-y-0 left-0 w-1 rounded-l ${deviceAccentClass}`} aria-hidden="true" />
|
||||||
{test.test_id}
|
{test.test_id}
|
||||||
|
|||||||
Reference in New Issue
Block a user