diff --git a/backend/app.py b/backend/app.py index dffd470..9a25ca3 100644 --- a/backend/app.py +++ b/backend/app.py @@ -106,6 +106,8 @@ class CompileScheduleRequest(BaseModel): start_date: str | None = Field(default=None, description="YYYY-MM-DD") rule: str = "" daytime_testing_today: bool = False + daytime_testing_hours: int = Field(default=8, ge=0, le=8) + daytime_testing_device: str = "" dual_device_weekend_start_enabled: bool = False dual_device_weekend_start_dates: list[str] = Field(default_factory=list) top_priority_tests_dut: list[str] = Field(default_factory=list) @@ -387,6 +389,12 @@ def compile_schedule_endpoint(request: CompileScheduleRequest) -> dict[str, Any] except ValueError as exc: raise HTTPException(status_code=400, detail="start_date must be YYYY-MM-DD") from exc + requested_daytime_device = str(request.daytime_testing_device or "").strip() + if not requested_daytime_device: + requested_daytime_device = DUT + if requested_daytime_device not in {DUT, REF}: + raise HTTPException(status_code=400, detail=f"daytime_testing_device must be {DUT} or {REF}") + all_tests = db.get_not_excluded_tests(DB_PATH, rule=request.rule) stored_tests = db.list_schedulable_tests(DB_PATH, rule=request.rule) if not stored_tests: @@ -444,6 +452,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, + daytime_testing_hours=request.daytime_testing_hours, + daytime_testing_device=requested_daytime_device, 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()), ) diff --git a/backend/scheduler.py b/backend/scheduler.py index 80c391f..0f8fce0 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -24,6 +24,7 @@ class ScheduleWindow: shifts: list[tuple[date, int]] capacity_minutes: int remaining_minutes: int + daytime_mode: bool = False assigned_device: str | None = None assigned_config: tuple[str, ...] | None = None @@ -37,6 +38,8 @@ class Scheduler: start_date: str | None = None, holiday_dates: set[str] = set(), daytime_testing_today: bool = False, + daytime_testing_hours: int = 8, + daytime_testing_device: str | None = None, dual_device_weekend_start_enabled: bool = False, dual_device_window_start_dates: set[str] | None = None, priority_weight: int = 1000, @@ -49,6 +52,9 @@ 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 + safe_daytime_hours = max(0, min(int(daytime_testing_hours), 8)) + self.daytime_testing_minutes = safe_daytime_hours * 60 + self.daytime_testing_device = daytime_testing_device if daytime_testing_device in {DUT, REF} else DUT 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 @@ -76,6 +82,7 @@ class Scheduler: window_index = 0 cursor_date = self.start_date + daytime_window_pending = self.daytime_testing_today and self.daytime_testing_minutes > 0 tc_order = self.get_tc_order(self.all_tests) @@ -85,7 +92,7 @@ class Scheduler: print(f"[scheduler] No bundles found for TC: {tc}. Skipping to next TC.") continue print(f"[scheduler] Scheduling bundles for TC: {tc} with {len(tc_bundles)} bundles.") - window_device = DUT + night_window_device = DUT pending_dut: list[TestBundle] = [] pending_ref: list[TestBundle] = [] @@ -98,17 +105,28 @@ class Scheduler: pending_ref.append(bundle) while pending_dut or pending_ref: - active_pending = pending_dut if window_device == DUT else pending_ref - if len(active_pending) == 0: - # If no unscheduled bundles for the current device, switch to the other device - window_device = REF if window_device == DUT else DUT - active_pending = pending_dut if window_device == DUT else pending_ref + daytime_window_active = daytime_window_pending and not is_off_day(cursor_date, self.holiday_dates) + window_device = self.daytime_testing_device if daytime_window_active else night_window_device - shifts, capacity = get_shift_sequence_with_capacity(cursor_date, self.holiday_dates, self.daytime_testing_today) + active_pending = pending_dut if window_device == DUT else pending_ref + if len(active_pending) == 0 and not daytime_window_active: + # If no unscheduled bundles for the current device, switch to the other device + night_window_device = REF if night_window_device == DUT else DUT + window_device = night_window_device + active_pending = pending_dut if night_window_device == DUT else pending_ref + + shifts, capacity = get_shift_sequence_with_capacity( + cursor_date, + self.holiday_dates, + daytime_window_active, + self.daytime_testing_minutes, + ) 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) + if daytime_window_active: + dual_device_window = False selected_bundles = [] selected_bundles = self._knapsack_select(capacity, active_pending) @@ -130,6 +148,7 @@ class Scheduler: shifts=shifts, capacity_minutes=capacity, remaining_minutes=remaining_time, + daytime_mode=daytime_window_active, assigned_config=tc, assigned_device=primary_device, ) @@ -148,8 +167,11 @@ class Scheduler: pending_ref = [b for b in pending_ref if (b.index, b.device) not in selected_keys] window_index += 1 - window_device = REF if window_device == DUT else DUT + if not daytime_window_active: + night_window_device = REF if night_window_device == DUT else DUT cursor_date = next_window_start_date(shifts) + if daytime_window_active: + daytime_window_pending = False return None @@ -250,7 +272,12 @@ class Scheduler: ) -> None: # Build per-shift remaining capacity shift_remaining = [ - get_shift_capacity_for_date(day, self.holiday_dates, self.daytime_testing_today).get(shift_idx, 0) + get_shift_capacity_for_date( + day, + self.holiday_dates, + window.daytime_mode, + self.daytime_testing_minutes, + ).get(shift_idx, 0) for day, shift_idx in window.shifts ] current_shift_pos = 0 diff --git a/backend/test_window.py b/backend/test_window.py index e21a890..15f148a 100644 --- a/backend/test_window.py +++ b/backend/test_window.py @@ -39,9 +39,11 @@ def get_shift_capacity_for_date( current_date: date, holiday_dates: set[str], daytime_shift2_only: bool = False, + daytime_shift2_minutes: int = 480, ) -> dict[int, int]: if daytime_shift2_only: - return {1: 0, 2: 480, 3: 0} + daytime_minutes = max(0, min(int(daytime_shift2_minutes), 480)) + return {1: 0, 2: daytime_minutes, 3: 0} if is_off_day(current_date, holiday_dates): return {1: 480, 2: 480, 3: 480} return {1: 480, 2: 0, 3: 480} @@ -51,10 +53,11 @@ def get_shift_sequence_with_capacity( current_date: date, holiday_dates: set[str], daytime_shift2_only: bool = False, + daytime_shift2_minutes: int = 480, ) -> tuple[list[tuple[date, int]], int]: shifts = get_shift_sequence(current_date, holiday_dates, daytime_shift2_only) capacity = sum( - get_shift_capacity_for_date(day, holiday_dates, daytime_shift2_only).get(shift_index, 0) + get_shift_capacity_for_date(day, holiday_dates, daytime_shift2_only, daytime_shift2_minutes).get(shift_index, 0) for day, shift_index in shifts ) return shifts, capacity diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a27596d..70ae181 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -245,6 +245,9 @@ function sanitizeSettings(saved = {}) { export default function App() { const [settingsOpen, setSettingsOpen] = useState(false) const [settings, setSettings] = useState(DEFAULT_SETTINGS) + const [daytimeTestingToday, setDaytimeTestingToday] = useState(false) + const [daytimeTestingHours, setDaytimeTestingHours] = useState('8') + const [daytimeTestingDevice, setDaytimeTestingDevice] = useState('CGW453') const [dualDeviceWeekendWeekSelections, setDualDeviceWeekendWeekSelections] = useState({}) const [topPriorityDut, setTopPriorityDut] = useState('') const [topPriorityRef, setTopPriorityRef] = useState('') @@ -291,6 +294,10 @@ export default function App() { if (!dualDeviceWeekendWeekEnabled) return [] return getWeekendStartDatesForWeek(weekStart, holidaySet) }, [dualDeviceWeekendWeekEnabled, weekStart, holidaySet]) + const daytimeDateKey = useMemo( + () => (daytimeTestingToday ? toKey(new Date()) : null), + [daytimeTestingToday], + ) // Fetch schedule for the given weekStart (Monday) const fetchSchedule = useCallback(async (start) => { @@ -413,6 +420,9 @@ export default function App() { setTopPriorityRef('') setLowestPriority('') setStartDateOverride('') + setDaytimeTestingToday(false) + setDaytimeTestingHours('8') + setDaytimeTestingDevice('CGW453') setFailedTests([]) setScheduleData({}) setPreviousScheduleData({}) @@ -435,9 +445,16 @@ export default function App() { setLoading(true) setError(null) try { + const parsedDaytimeHours = Number.parseInt(String(daytimeTestingHours).trim(), 10) + const safeDaytimeHours = Number.isFinite(parsedDaytimeHours) + ? Math.max(0, Math.min(8, parsedDaytimeHours)) + : 8 const effectiveStartDate = startDateOverride.trim() const result = await api.compileSchedule({ start_date: effectiveStartDate || null, + daytime_testing_today: daytimeTestingToday, + daytime_testing_hours: safeDaytimeHours, + daytime_testing_device: daytimeTestingDevice, dual_device_weekend_start_dates: dualDeviceWindowStartDates, top_priority_tests_dut: topPriorityDut.split(',').map(s => s.trim()).filter(Boolean), top_priority_tests_ref: topPriorityRef.split(',').map(s => s.trim()).filter(Boolean), @@ -467,7 +484,7 @@ export default function App() { } } - function handleRerunDecision(_rerunDuringDay) { + function handleRerunDecision(rerunDuringDay) { const appendUnique = (currentValue, incomingValues) => { const existing = currentValue.split(',').map(s => s.trim()).filter(Boolean) const merged = [...new Set([...existing, ...incomingValues])] @@ -488,6 +505,15 @@ export default function App() { setTopPriorityRef(prev => appendUnique(prev, refIncoming)) } + if (rerunDuringDay) { + setDaytimeTestingToday(true) + if (dutIncoming.length > 0 && refIncoming.length === 0) { + setDaytimeTestingDevice('CGW453') + } else if (refIncoming.length > 0 && dutIncoming.length === 0) { + setDaytimeTestingDevice('CGW452') + } + } + setFailedTests([]) } @@ -521,13 +547,14 @@ export default function App() { setFailedTests([])} />
`${device}: ${ids.join(', ')}`) return ( -
+
{deviceSummaries.map((summary) => ( -

+

{summary}

))} -

- Estimated rerun time: {timeStr} - {' '}— Schedule rerun during daytime today? -

-
-
- -
) diff --git a/frontend/src/components/RightPanel.jsx b/frontend/src/components/RightPanel.jsx index 2f27922..821df94 100644 --- a/frontend/src/components/RightPanel.jsx +++ b/frontend/src/components/RightPanel.jsx @@ -1,3 +1,4 @@ +import { useState } from 'react' import cgw453Image from '../assets/CGW453.PNG' function formatCompletionDate(value) { @@ -16,6 +17,12 @@ function formatCompletionDate(value) { export default function RightPanel({ completionDate, + daytimeTestingToday, + onDaytimeTestingTodayChange, + daytimeTestingHours, + onDaytimeTestingHoursChange, + daytimeTestingDevice, + onDaytimeTestingDeviceChange, startDateOverride, onStartDateOverrideChange, topPriorityDut, @@ -29,6 +36,7 @@ export default function RightPanel({ loading = false, }) { const formattedCompletionDate = formatCompletionDate(completionDate) + const [topPriorityOpen, setTopPriorityOpen] = useState(false) return (
- {/* Top priority */}
- -