From 96a6309ff682cf06accc61f65e42c554f71f4616 Mon Sep 17 00:00:00 2001
From: Mia Wu
Date: Mon, 13 Jul 2026 04:02:23 -0400
Subject: [PATCH] fix dual device and calendar display bug
---
.gitignore | 1 +
backend/.env | 2 -
backend/db.py | 35 ++++--
backend/scheduler.py | 29 +++--
backend/test_runtime_overrides.py | 62 ----------
backend/test_scheduler_dual_device.py | 111 ------------------
frontend/src/components/DayColumn.jsx | 7 +-
frontend/src/components/SettingsModal.jsx | 4 +-
frontend/src/components/ShiftSlot.jsx | 7 +-
.../src/components/TestWindowDetailsPanel.jsx | 15 ++-
10 files changed, 71 insertions(+), 202 deletions(-)
delete mode 100644 backend/.env
delete mode 100644 backend/test_runtime_overrides.py
delete mode 100644 backend/test_scheduler_dual_device.py
diff --git a/.gitignore b/.gitignore
index ded0387..b479b1c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
/backend/__pycache__
/backend/_run_sched_test.py
/backend/output.txt
+/backend/.env
IMPLEMENTATIONPLAN.md
scheduler.db
scheduler.db*
\ No newline at end of file
diff --git a/backend/.env b/backend/.env
deleted file mode 100644
index bc8f05d..0000000
--- a/backend/.env
+++ /dev/null
@@ -1,2 +0,0 @@
-DUT="CGW453"
-REF="CGW452"
\ No newline at end of file
diff --git a/backend/db.py b/backend/db.py
index 3927705..225ecf3 100644
--- a/backend/db.py
+++ b/backend/db.py
@@ -123,14 +123,6 @@ def init_db(db_path: str | Path = DB_PATH) -> None:
minutes INTEGER NOT NULL
);
- CREATE TABLE IF NOT EXISTS rerun_events (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- detected_date TEXT NOT NULL,
- failed_test_ids_json TEXT NOT NULL,
- estimated_rerun_minutes INTEGER NOT NULL,
- rerun_during_day INTEGER
- );
-
CREATE INDEX IF NOT EXISTS idx_tests_status ON tests(status);
CREATE INDEX IF NOT EXISTS idx_tests_priority ON tests(priority);
CREATE INDEX IF NOT EXISTS idx_schedules_date_shift ON schedules(scheduled_date, shift_index);
@@ -503,11 +495,32 @@ def get_schedule_week(start_date: str, db_path: str | Path = DB_PATH) -> list[Sc
FROM schedules s
JOIN tests t ON t.test_id = s.test_id AND t.device = s.device
WHERE s.schedule_version = ?
- AND s.scheduled_date >= ?
- AND s.scheduled_date < date(?, '+7 day')
+ AND (
+ (
+ s.scheduled_date >= ?
+ AND s.scheduled_date < date(?, '+7 day')
+ AND s.shift_index IN (2, 3)
+ )
+ OR (
+ s.scheduled_date > ?
+ AND s.scheduled_date < date(?, '+7 day')
+ AND s.shift_index = 1
+ )
+ OR (
+ s.scheduled_date = date(?, '+7 day')
+ AND s.shift_index = 1
+ )
+ )
ORDER BY s.scheduled_date, s.shift_index, s.sequence_in_shift
""",
- (latest, start_date, start_date),
+ (
+ latest,
+ start_date,
+ start_date,
+ start_date,
+ start_date,
+ start_date,
+ ),
).fetchall()
result: list[ScheduleRow] = []
diff --git a/backend/scheduler.py b/backend/scheduler.py
index e3494bf..6832feb 100644
--- a/backend/scheduler.py
+++ b/backend/scheduler.py
@@ -293,7 +293,7 @@ class Scheduler:
for day, shift_idx in window.shifts
]
current_shift_pos = 0
- sequence_in_shift = 0
+ sequence_in_shift = [0] * len(window.shifts)
for bundle in bundles:
bundle_key = (bundle.index, bundle.device)
@@ -309,26 +309,39 @@ class Scheduler:
test_minutes = self._get_test_minutes(test, bundle.device)
- # Advance past any shift that can't fit this test
- while current_shift_pos < len(window.shifts) and shift_remaining[current_shift_pos] < test_minutes:
+ while current_shift_pos < len(window.shifts) and shift_remaining[current_shift_pos] <= 0:
current_shift_pos += 1
- sequence_in_shift = 0
if current_shift_pos >= len(window.shifts):
break
- shift_day, shift_idx = window.shifts[current_shift_pos]
- sequence_in_shift += 1
- shift_remaining[current_shift_pos] -= test_minutes
+ if sum(shift_remaining[current_shift_pos:]) < test_minutes:
+ break
+
+ start_shift_pos = current_shift_pos
+ shift_day, shift_idx = window.shifts[start_shift_pos]
+ sequence_in_shift[start_shift_pos] += 1
+
+ remaining_test_minutes = test_minutes
+ consume_shift_pos = start_shift_pos
+ while remaining_test_minutes > 0 and consume_shift_pos < len(window.shifts):
+ consumed_minutes = min(shift_remaining[consume_shift_pos], remaining_test_minutes)
+ shift_remaining[consume_shift_pos] -= consumed_minutes
+ remaining_test_minutes -= consumed_minutes
+ consume_shift_pos += 1
+
window.remaining_minutes -= test_minutes
+ while current_shift_pos < len(window.shifts) and shift_remaining[current_shift_pos] <= 0:
+ current_shift_pos += 1
+
self.schedule.append(
ScheduleEntry(
test_id=test,
device=bundle.device,
scheduled_date=str(shift_day),
shift_index=shift_idx,
- sequence_in_shift=sequence_in_shift,
+ sequence_in_shift=sequence_in_shift[start_shift_pos],
)
)
diff --git a/backend/test_runtime_overrides.py b/backend/test_runtime_overrides.py
deleted file mode 100644
index 5757ba7..0000000
--- a/backend/test_runtime_overrides.py
+++ /dev/null
@@ -1,62 +0,0 @@
-from __future__ import annotations
-
-import csv
-from pathlib import Path
-import sys
-from tempfile import TemporaryDirectory
-import unittest
-
-sys.path.append(str(Path(__file__).resolve().parent))
-
-from db import DEVICE_DUT, DEVICE_REF
-from parser import P2P_COE_REQUIRED_COLUMNS, parse_target_csv
-
-
-def _write_p2p_csv(directory: str, test_id: str = "P2PRXAX001") -> Path:
- path = Path(directory) / "runtime-overrides.csv"
- row = {column: "" for column in P2P_COE_REQUIRED_COLUMNS}
- row["Priority"] = "1"
- row["Index"] = "1"
- row["TC ID"] = test_id
-
- with path.open("w", encoding="utf-8", newline="") as handle:
- writer = csv.DictWriter(handle, fieldnames=P2P_COE_REQUIRED_COLUMNS)
- writer.writeheader()
- writer.writerow(row)
-
- return path
-
-
-class RuntimeOverridesParseTests(unittest.TestCase):
- def test_parse_target_csv_applies_device_specific_runtime_overrides(self) -> None:
- with TemporaryDirectory() as tmpdir:
- csv_path = _write_p2p_csv(tmpdir)
-
- parsed = parse_target_csv(
- csv_path,
- runtime_overrides={
- DEVICE_DUT: {"P2P": 91},
- DEVICE_REF: {"P2P": 123},
- },
- )
-
- by_device = {record.device: record for record in parsed.tests}
- self.assertEqual(by_device[DEVICE_DUT].estimated_minutes, 91)
- self.assertEqual(by_device[DEVICE_REF].estimated_minutes, 123)
-
- def test_parse_target_csv_keeps_legacy_runtime_override_for_both_devices(self) -> None:
- with TemporaryDirectory() as tmpdir:
- csv_path = _write_p2p_csv(tmpdir)
-
- parsed = parse_target_csv(
- csv_path,
- runtime_overrides={"P2P": 88},
- )
-
- by_device = {record.device: record for record in parsed.tests}
- self.assertEqual(by_device[DEVICE_DUT].estimated_minutes, 88)
- self.assertEqual(by_device[DEVICE_REF].estimated_minutes, 88)
-
-
-if __name__ == "__main__":
- unittest.main()
\ No newline at end of file
diff --git a/backend/test_scheduler_dual_device.py b/backend/test_scheduler_dual_device.py
deleted file mode 100644
index cd70e0c..0000000
--- a/backend/test_scheduler_dual_device.py
+++ /dev/null
@@ -1,111 +0,0 @@
-from __future__ import annotations
-
-from pathlib import Path
-import sys
-import unittest
-
-sys.path.append(str(Path(__file__).resolve().parent))
-
-from scheduler import DUT, REF, Scheduler
-from test_bundle import Test
-
-
-def _test_config() -> dict[str, dict[str, str | None]]:
- return {
- "Station 1": {
- "test_point": "T1D",
- "sta": "STA5",
- }
- }
-
-
-def _make_test(test_id: str, device: str, minutes: int) -> Test:
- return Test(
- test_id=test_id,
- device=device,
- test_type="P2P",
- rotation=None,
- rx_tx=None,
- has_coe_pair=False,
- coe_pairing=[],
- config=_test_config(),
- throttled=False,
- estimated_minutes=minutes,
- )
-
-
-class SchedulerDualDeviceWeekendStartTests(unittest.TestCase):
- def _first_window_devices(self, entries, start_date: str) -> set[str]:
- return {
- entry.device
- for entry in entries
- if entry.scheduled_date == start_date and entry.shift_index == 3
- }
-
- def test_weekend_start_window_stays_single_device_when_disabled(self) -> None:
- start_date = "2026-07-10" # Friday
- tests = [
- _make_test("P2PRXAX001", DUT, 900),
- _make_test("P2PRXAX001", REF, 300),
- ]
-
- scheduler = Scheduler(
- tests=tests,
- top_priority_tests=set(),
- start_date=start_date,
- holiday_dates=set(),
- dual_device_weekend_start_enabled=False,
- )
-
- err = scheduler.compile_schedule()
- self.assertIsNone(err)
- entries = scheduler.get_schedule()
-
- self.assertEqual(self._first_window_devices(entries, start_date), {DUT})
-
- def test_weekend_start_window_runs_both_devices_when_enabled(self) -> None:
- start_date = "2026-07-10" # Friday
- tests = [
- _make_test("P2PRXAX001", DUT, 900),
- _make_test("P2PRXAX001", REF, 300),
- ]
-
- scheduler = Scheduler(
- tests=tests,
- top_priority_tests=set(),
- start_date=start_date,
- holiday_dates=set(),
- dual_device_weekend_start_enabled=True,
- )
-
- err = scheduler.compile_schedule()
- self.assertIsNone(err)
- entries = scheduler.get_schedule()
-
- self.assertEqual(self._first_window_devices(entries, start_date), {DUT, REF})
-
- def test_holiday_adjusted_weekend_start_runs_both_devices(self) -> None:
- start_date = "2026-07-09" # Thursday with Friday as holiday => weekend-start window
- holidays = {"2026-07-10"}
- tests = [
- _make_test("P2PRXAX001", DUT, 700),
- _make_test("P2PRXAX001", REF, 400),
- ]
-
- scheduler = Scheduler(
- tests=tests,
- top_priority_tests=set(),
- start_date=start_date,
- holiday_dates=holidays,
- dual_device_weekend_start_enabled=True,
- )
-
- err = scheduler.compile_schedule()
- self.assertIsNone(err)
- entries = scheduler.get_schedule()
-
- self.assertEqual(self._first_window_devices(entries, start_date), {DUT, REF})
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/frontend/src/components/DayColumn.jsx b/frontend/src/components/DayColumn.jsx
index 120349f..e360dee 100644
--- a/frontend/src/components/DayColumn.jsx
+++ b/frontend/src/components/DayColumn.jsx
@@ -18,11 +18,16 @@ export default function DayColumn({
const dayName = DAY_NAMES[date.getDay()]
const dayNum = date.getDate()
const dateKey = date.toISOString().slice(0, 10)
+ const nextDateKey = new Date(date.getTime() + (24 * 60 * 60 * 1000)).toISOString().slice(0, 10)
function getWindowForShift(shiftIndex) {
return windowLookup.get(`${dateKey}::shift${shiftIndex}`) ?? null
}
+ function getNextDayShift1Window() {
+ return windowLookup.get(`${nextDateKey}::shift1`) ?? null
+ }
+
const shift2Visible = showShift2 || shifts.shift2.length > 0 || Boolean(getWindowForShift(2))
return (
@@ -65,7 +70,7 @@ export default function DayColumn({
tests={nextDayShift1}
visible={true}
active={nextDayShift1Active}
- windowDetails={null}
+ windowDetails={getNextDayShift1Window()}
minContentRows={shiftMinRows.nextDayShift1}
slotHeightPx={slotHeights.nextDayShift1}
onWindowSelect={onWindowSelect}
diff --git a/frontend/src/components/SettingsModal.jsx b/frontend/src/components/SettingsModal.jsx
index cb41c11..924fb71 100644
--- a/frontend/src/components/SettingsModal.jsx
+++ b/frontend/src/components/SettingsModal.jsx
@@ -259,9 +259,9 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
Holidays
- set('holidays', e.target.value)}
diff --git a/frontend/src/components/ShiftSlot.jsx b/frontend/src/components/ShiftSlot.jsx
index e7a2551..3bc5756 100644
--- a/frontend/src/components/ShiftSlot.jsx
+++ b/frontend/src/components/ShiftSlot.jsx
@@ -42,7 +42,12 @@ export default function ShiftSlot({
style={{ minHeight: `${minContentHeightPx}px` }}
>
{tests.length === 0 ? null : (
- tests.map((test) => )
+ tests.map((test, index) => (
+
+ ))
)}
diff --git a/frontend/src/components/TestWindowDetailsPanel.jsx b/frontend/src/components/TestWindowDetailsPanel.jsx
index fefe262..e2275e4 100644
--- a/frontend/src/components/TestWindowDetailsPanel.jsx
+++ b/frontend/src/components/TestWindowDetailsPanel.jsx
@@ -183,6 +183,10 @@ function TestRow({ test }) {
export default function TestWindowDetailsPanel({ isOpen, windowDetails, onClose }) {
const [showConfigMap, setShowConfigMap] = useState(false)
+ const windowTests = useMemo(
+ () => (Array.isArray(windowDetails?.tests) ? windowDetails.tests : []),
+ [windowDetails],
+ )
const windowTestConfig = useMemo(() => getWindowTestConfig(windowDetails), [windowDetails])
const windowConfigMapKey = useMemo(
() => (windowTestConfig.length > 0 ? String(windowTestConfig[0]).toUpperCase() : null),
@@ -311,12 +315,15 @@ export default function TestWindowDetailsPanel({ isOpen, windowDetails, onClose
Tests Scheduled In This Window
-
{windowDetails.tests?.length ?? 0} tests
+
{windowTests.length} tests
- {windowDetails.tests?.length ? (
+ {windowTests.length ? (
- {windowDetails.tests.map((test) => (
-
+ {windowTests.map((test, index) => (
+
))}
) : (