From 31caa13c3dc6115728f2262c732c9854b68e4216 Mon Sep 17 00:00:00 2001
From: Mia Wu
Date: Sun, 12 Jul 2026 18:34:28 -0400
Subject: [PATCH] Dual device on weekend feature
---
backend/app.py | 4 +
backend/scheduler.py | 37 ++++++++-
backend/test_scheduler_dual_device.py | 111 +++++++++++++++++++++++++
frontend/src/App.jsx | 55 +++++++++---
frontend/src/components/Calendar.jsx | 21 +++++
frontend/src/components/RightPanel.jsx | 25 ------
6 files changed, 214 insertions(+), 39 deletions(-)
create mode 100644 backend/test_scheduler_dual_device.py
diff --git a/backend/app.py b/backend/app.py
index 0710031..92b63a1 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -75,6 +75,8 @@ class CompileScheduleRequest(BaseModel):
start_date: str | None = Field(default=None, description="YYYY-MM-DD")
rule: str = ""
daytime_testing_today: bool = False
+ dual_device_weekend_start_enabled: bool = False
+ dual_device_weekend_start_dates: list[str] = Field(default_factory=list)
top_priority_tests: list[str] = Field(default_factory=list)
lowest_priority_tests: list[str] = Field(default_factory=list)
@@ -378,6 +380,8 @@ def compile_schedule_endpoint(request: CompileScheduleRequest) -> dict[str, Any]
start_date=request.start_date,
holiday_dates=holiday_dates,
daytime_testing_today=request.daytime_testing_today,
+ dual_device_weekend_start_enabled=request.dual_device_weekend_start_enabled,
+ dual_device_window_start_dates=set(item.strip() for item in request.dual_device_weekend_start_dates if item.strip()),
)
try:
diff --git a/backend/scheduler.py b/backend/scheduler.py
index d564646..52f486e 100644
--- a/backend/scheduler.py
+++ b/backend/scheduler.py
@@ -2,8 +2,8 @@ from __future__ import annotations
import os
from dataclasses import dataclass
-from datetime import date
-from test_window import get_shift_sequence_with_capacity, next_window_start_date
+from datetime import date, timedelta
+from test_window import get_shift_sequence_with_capacity, is_off_day, next_window_start_date
from test_bundle import Test, TestBundle, build_test_bundles, bundle_pair_lookup
DUT = (os.getenv("DUT") or "DUT").strip()
@@ -36,6 +36,8 @@ class Scheduler:
start_date: str | None = None,
holiday_dates: set[str] = set(),
daytime_testing_today: bool = False,
+ dual_device_weekend_start_enabled: bool = False,
+ dual_device_window_start_dates: set[str] | None = None,
priority_weight: int = 100,
):
self.top_priority_tests = top_priority_tests
@@ -45,6 +47,8 @@ class Scheduler:
self.start_date = date.fromisoformat(start_date) if start_date else date.today()
self.holiday_dates = holiday_dates
self.daytime_testing_today = daytime_testing_today
+ self.dual_device_weekend_start_enabled = dual_device_weekend_start_enabled
+ self.dual_device_window_start_dates = dual_device_window_start_dates or set()
self.priority_weight = priority_weight
self.pending_dut_mirror: list[TestBundle] = []
self.pending_ref_mirror: list[TestBundle] = []
@@ -102,6 +106,10 @@ class Scheduler:
active_unscheduled = dut_unscheduled if window_device == DUT else ref_unscheduled
shifts, capacity = get_shift_sequence_with_capacity(cursor_date, self.holiday_dates, self.daytime_testing_today)
+ cursor_date_key = cursor_date.isoformat()
+ dual_device_window = cursor_date_key in self.dual_device_window_start_dates
+ if not dual_device_window and self.dual_device_weekend_start_enabled:
+ dual_device_window = self._is_weekend_start_day(cursor_date)
# Evaluate both devices without mutating queue state, then commit once.
mirrored_bundles, knapsack_bundles, remaining_time = self._select_bundles(
@@ -114,6 +122,7 @@ class Scheduler:
selected_bundles = mirrored_bundles + knapsack_bundles
if len(selected_bundles) == 0:
window_device = REF if window_device == DUT else DUT
+ active_unscheduled = dut_unscheduled if window_device == DUT else ref_unscheduled
mirrored_bundles, knapsack_bundles, remaining_time = self._select_bundles(
window_device,
capacity,
@@ -125,6 +134,20 @@ class Scheduler:
if len(selected_bundles) == 0:
break
+ primary_device = window_device
+
+ if dual_device_window and remaining_time > 0:
+ secondary_device = REF if primary_device == DUT else DUT
+ secondary_unscheduled = dut_unscheduled if secondary_device == DUT else ref_unscheduled
+ secondary_mirrored, secondary_knapsack, remaining_time = self._select_bundles(
+ secondary_device,
+ remaining_time,
+ secondary_unscheduled,
+ tc,
+ mutate=True,
+ )
+ selected_bundles.extend(secondary_mirrored + secondary_knapsack)
+
# Create a mirror of the other device for the next window
bundle_pairs = bundle_pair_lookup(knapsack_bundles, dut_unscheduled, ref_unscheduled)
@@ -134,9 +157,10 @@ class Scheduler:
pending_target = self.pending_dut_mirror
existing_pending = {(b.index, b.device) for b in pending_target}
+ selected_bundle_keys = {(b.index, b.device) for b in selected_bundles}
for pair_bundle in bundle_pairs:
pair_key = (pair_bundle.index, pair_bundle.device)
- if pair_key in self.scheduled_bundle_keys or pair_key in existing_pending:
+ if pair_key in self.scheduled_bundle_keys or pair_key in existing_pending or pair_key in selected_bundle_keys:
continue
pending_target.append(pair_bundle)
existing_pending.add(pair_key)
@@ -148,7 +172,7 @@ class Scheduler:
capacity_minutes=capacity,
remaining_minutes=remaining_time,
assigned_config=tc,
- assigned_device=window_device,
+ assigned_device=primary_device,
)
self._place_bundles_in_window(window, selected_bundles)
@@ -170,6 +194,11 @@ class Scheduler:
return None
+ def _is_weekend_start_day(self, current_date: date) -> bool:
+ if is_off_day(current_date, self.holiday_dates):
+ return False
+ return is_off_day(current_date + timedelta(days=1), self.holiday_dates)
+
def _select_bundles(
self,
device: str,
diff --git a/backend/test_scheduler_dual_device.py b/backend/test_scheduler_dual_device.py
new file mode 100644
index 0000000..cd70e0c
--- /dev/null
+++ b/backend/test_scheduler_dual_device.py
@@ -0,0 +1,111 @@
+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/App.jsx b/frontend/src/App.jsx
index 84d8451..ef2c939 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -37,6 +37,33 @@ function addDays(date, n) {
return d
}
+function parseHolidayDates(raw = '') {
+ return new Set(
+ String(raw)
+ .split(',')
+ .map((token) => token.trim())
+ .filter(Boolean),
+ )
+}
+
+function isOffDay(date, holidaySet) {
+ const weekday = date.getDay()
+ if (weekday === 0 || weekday === 6) return true
+ return holidaySet.has(toKey(date))
+}
+
+function getWeekendStartDatesForWeek(weekStart, holidaySet) {
+ const dates = []
+ for (let offset = 0; offset < 7; offset += 1) {
+ const current = addDays(weekStart, offset)
+ const nextDay = addDays(current, 1)
+ if (!isOffDay(current, holidaySet) && isOffDay(nextDay, holidaySet)) {
+ dates.push(toKey(current))
+ }
+ }
+ return dates
+}
+
function getTonightWindowKeys() {
const now = new Date()
const hour = now.getHours()
@@ -120,7 +147,7 @@ function sanitizeSettings(saved = {}) {
export default function App() {
const [settingsOpen, setSettingsOpen] = useState(false)
const [settings, setSettings] = useState(DEFAULT_SETTINGS)
- const [daytimeEnabled, setDaytimeEnabled] = useState(false)
+ const [dualDeviceWeekendWeekSelections, setDualDeviceWeekendWeekSelections] = useState({})
const [topPriority, setTopPriority] = useState('')
const [lowestPriority, setLowestPriority] = useState('')
const [startDateOverride, setStartDateOverride] = useState('')
@@ -148,6 +175,13 @@ export default function App() {
() => scheduleWindows.find((windowDetails) => windowDetails.window_id === selectedWindowId) ?? null,
[scheduleWindows, selectedWindowId],
)
+ const holidaySet = useMemo(() => parseHolidayDates(settings.holidays), [settings.holidays])
+ const activeWeekKey = useMemo(() => toKey(weekStart), [weekStart])
+ const dualDeviceWeekendWeekEnabled = Boolean(dualDeviceWeekendWeekSelections[activeWeekKey])
+ const dualDeviceWindowStartDates = useMemo(() => {
+ if (!dualDeviceWeekendWeekEnabled) return []
+ return getWeekendStartDatesForWeek(weekStart, holidaySet)
+ }, [dualDeviceWeekendWeekEnabled, weekStart, holidaySet])
// Fetch schedule for the given weekStart (Monday)
const fetchSchedule = useCallback(async (start) => {
@@ -245,7 +279,7 @@ export default function App() {
const effectiveStartDate = startDateOverride.trim()
const result = await api.compileSchedule({
start_date: effectiveStartDate || null,
- daytime_testing_today: daytimeEnabled,
+ dual_device_weekend_start_dates: dualDeviceWindowStartDates,
top_priority_tests: topPriority.split(',').map(s => s.trim()).filter(Boolean),
lowest_priority_tests: lowestPriority.split(',').map(s => s.trim()).filter(Boolean),
rule: settings.testExclusion ?? '',
@@ -273,17 +307,13 @@ export default function App() {
}
}
- function handleRerunDecision(rerunDuringDay) {
- const rerunIds = failedTests.map(t => t.test_id).join(', ')
+ function handleRerunDecision(_rerunDuringDay) {
setTopPriority(prev => {
const existing = prev.split(',').map(s => s.trim()).filter(Boolean)
const incoming = failedTests.map(t => t.test_id)
const merged = [...new Set([...existing, ...incoming])]
return merged.join(', ')
})
- if (rerunDuringDay) {
- setDaytimeEnabled(true)
- }
setFailedTests([])
}
@@ -323,9 +353,16 @@ export default function App() {
{
+ setDualDeviceWeekendWeekSelections((prev) => ({
+ ...prev,
+ [activeWeekKey]: enabled,
+ }))
+ }}
windowLookup={windowLookup}
onWindowSelect={handleWindowSelect}
/>
@@ -335,8 +372,6 @@ export default function App() {
completionDate={completionDate}
startDateOverride={startDateOverride}
onStartDateOverrideChange={setStartDateOverride}
- daytimeEnabled={daytimeEnabled}
- onDaytimeEnabledChange={setDaytimeEnabled}
topPriority={topPriority}
onTopPriorityChange={setTopPriority}
lowestPriority={lowestPriority}
diff --git a/frontend/src/components/Calendar.jsx b/frontend/src/components/Calendar.jsx
index 70ecbf9..8a7f8d0 100644
--- a/frontend/src/components/Calendar.jsx
+++ b/frontend/src/components/Calendar.jsx
@@ -53,6 +53,8 @@ export default function Calendar({
daytimeDateKey = null,
weekStart,
onWeekChange,
+ dualDeviceWeekendWeekEnabled = false,
+ onDualDeviceWeekendWeekEnabledChange,
windowLookup = new Map(),
onWindowSelect,
}) {
@@ -111,6 +113,25 @@ export default function Calendar({
>
Today
+
+ Run both devices on weekend this week
+
+
{/* 7-day grid */}
diff --git a/frontend/src/components/RightPanel.jsx b/frontend/src/components/RightPanel.jsx
index 4e8c979..c8cb8db 100644
--- a/frontend/src/components/RightPanel.jsx
+++ b/frontend/src/components/RightPanel.jsx
@@ -16,8 +16,6 @@ export default function RightPanel({
completionDate,
startDateOverride,
onStartDateOverrideChange,
- daytimeEnabled,
- onDaytimeEnabledChange,
topPriority,
onTopPriorityChange,
lowestPriority,
@@ -40,29 +38,6 @@ export default function RightPanel({
- {/* Daytime testing toggle */}
-
-
-
- Day Time Testing (Today)
-
-
-
-
-
{/* Top priority */}