implemented watcher and rerun logic

This commit is contained in:
2026-06-25 11:31:20 -04:00
parent 21402f7ee3
commit be13849a4f
15 changed files with 871 additions and 97 deletions
+91 -17
View File
@@ -17,6 +17,17 @@ function getMondayOfWeek(date) {
return d
}
function parseLocalDate(value) {
if (!value) return null
const [year, month, day] = value.split('-').map(Number)
if (!year || !month || !day) return null
const parsed = new Date(year, month - 1, day)
parsed.setHours(0, 0, 0, 0)
return Number.isNaN(parsed.getTime()) ? null : parsed
}
function toKey(d) { return d.toISOString().slice(0, 10) }
function addDays(date, n) {
@@ -85,9 +96,22 @@ const DEFAULT_SETTINGS = {
p3pCsvPath: '',
refResultDir: '',
dutResultDir: '',
smbUsername: '',
smbPassword: '',
smbDomain: '',
p2pRuntimeMinutes: '',
coeRuntimeMinutes: '',
p3pRuntimeMinutes: '',
testExclusion: '',
holidays: '',
startDateOverride: '',
}
function sanitizeSettings(saved = {}) {
const { startDateOverride: _ignored, ...rest } = saved
return {
...DEFAULT_SETTINGS,
...rest,
}
}
// ---------------------------------------------------------------------------
@@ -98,6 +122,7 @@ export default function App() {
const [daytimeEnabled, setDaytimeEnabled] = useState(false)
const [topPriority, setTopPriority] = useState('')
const [lowestPriority, setLowestPriority] = useState('')
const [startDateOverride, setStartDateOverride] = useState('')
const [failedTests, setFailedTests] = useState([])
const [scheduleData, setScheduleData] = useState({})
const [completionDate, setCompletionDate] = useState(null)
@@ -116,6 +141,15 @@ export default function App() {
}
}, [])
const fetchRerunTests = useCallback(async () => {
try {
const data = await api.getRerunTests()
setFailedTests(data.tests ?? [])
} catch (e) {
console.error('Failed to fetch rerun tests:', e)
}
}, [])
// On mount: load settings + holidays + schedule for today's week
useEffect(() => {
async function init() {
@@ -124,9 +158,8 @@ export default function App() {
api.getSettings(),
api.getHolidays(),
])
setSettings(prev => ({
...prev,
...saved,
setSettings(() => ({
...sanitizeSettings(saved),
holidays: (holidayData.dates ?? []).join(', '),
}))
} catch (e) {
@@ -142,26 +175,43 @@ export default function App() {
fetchSchedule(weekStart)
}, [weekStart, fetchSchedule])
// Keep calendar statuses fresh when backend watcher marks tests from new result folders.
useEffect(() => {
const timer = setInterval(() => {
fetchSchedule(weekStart)
fetchRerunTests()
}, 5000)
return () => clearInterval(timer)
}, [weekStart, fetchSchedule, fetchRerunTests])
// Load rerun tests on mount
useEffect(() => {
fetchRerunTests()
}, [fetchRerunTests])
// -------------------------------------------------------------------------
async function handleSaveSettings(newSettings) {
setLoading(true)
setError(null)
try {
await api.saveSettings(newSettings)
const sanitizedSettings = sanitizeSettings(newSettings)
const holidayDates = (newSettings.holidays ?? '')
await api.saveSettings(sanitizedSettings)
const holidayDates = (sanitizedSettings.holidays ?? '')
.split(',').map(s => s.trim()).filter(Boolean)
await api.saveHolidays(holidayDates)
if (newSettings.p2pCoeCsvPath?.trim()) {
await api.loadCsv(newSettings.p2pCoeCsvPath.trim())
if (sanitizedSettings.p2pCoeCsvPath?.trim()) {
await api.loadCsv(sanitizedSettings.p2pCoeCsvPath.trim())
}
if (newSettings.p3pCsvPath?.trim()) {
await api.loadCsv(newSettings.p3pCsvPath.trim())
if (sanitizedSettings.p3pCsvPath?.trim()) {
await api.loadCsv(sanitizedSettings.p3pCsvPath.trim())
}
setSettings(newSettings)
setSettings(sanitizedSettings)
} catch (e) {
setError(e.message)
} finally {
@@ -173,15 +223,30 @@ export default function App() {
setLoading(true)
setError(null)
try {
const effectiveStartDate = startDateOverride.trim()
const result = await api.compileSchedule({
start_date: settings.startDateOverride?.trim() || null,
start_date: effectiveStartDate || null,
daytime_testing_today: daytimeEnabled,
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 ?? '',
})
setCompletionDate(result.completion_date ?? null)
await fetchSchedule(weekStart)
if (effectiveStartDate) {
const overrideDate = parseLocalDate(effectiveStartDate)
if (overrideDate) {
const overrideWeekStart = getMondayOfWeek(overrideDate)
setWeekStart(overrideWeekStart)
await fetchSchedule(overrideWeekStart)
} else {
await fetchSchedule(weekStart)
}
} else {
await fetchSchedule(weekStart)
}
setStartDateOverride('')
} catch (e) {
setError(e.message)
} finally {
@@ -190,8 +255,16 @@ export default function App() {
}
function handleRerunDecision(rerunDuringDay) {
// TODO: POST /api/failed-tests/rerun-decision once backend endpoint exists
console.log('Rerun during day:', rerunDuringDay)
const rerunIds = failedTests.map(t => t.test_id).join(', ')
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([])
}
@@ -209,8 +282,7 @@ export default function App() {
)}
<FailedBanner
failedTests={failedTests}
estimatedMinutes={0}
rerunTests={failedTests}
onDecision={handleRerunDecision}
/>
@@ -226,6 +298,8 @@ export default function App() {
<RightPanel
completionDate={completionDate}
startDateOverride={startDateOverride}
onStartDateOverrideChange={setStartDateOverride}
daytimeEnabled={daytimeEnabled}
onDaytimeEnabledChange={setDaytimeEnabled}
topPriority={topPriority}
+1
View File
@@ -31,6 +31,7 @@ export const api = {
// Schedule
compileSchedule: (opts) => request('POST', '/schedule/compile', opts),
getScheduleWeek: (start) => request('GET', `/schedule/week?start=${start}`),
getRerunTests: () => request('GET', '/tests/rerun'),
}
// Transform the flat items array from GET /api/schedule/week into the
+1 -2
View File
@@ -121,8 +121,7 @@ export default function Calendar({ scheduleData = {}, daytimeDateKey = null, wee
<div className="flex gap-4 text-xs text-gray-400">
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-gray-500 inline-block" />Pending</span>
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-green-700 inline-block" />Completed</span>
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-red-700 inline-block" />Failed</span>
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-yellow-700 inline-block" />Invalid</span>
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-red-700 inline-block" />Rerun Required</span>
</div>
</div>
)
+10 -8
View File
@@ -1,9 +1,11 @@
export default function FailedBanner({ failedTests, estimatedMinutes, onDecision }) {
if (!failedTests || failedTests.length === 0) return null
export default function FailedBanner({ rerunTests, onDecision }) {
if (!rerunTests || rerunTests.length === 0) return null
const hours = Math.floor(estimatedMinutes / 60)
const mins = estimatedMinutes % 60
const totalMinutes = rerunTests.reduce((sum, t) => sum + (t.estimated_minutes ?? 0), 0)
const hours = Math.floor(totalMinutes / 60)
const mins = totalMinutes % 60
const timeStr = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`
const testIds = rerunTests.map(t => t.test_id).join(', ')
return (
<div className="flex items-start gap-4 px-6 py-3 bg-red-900/70 border-b border-red-700 text-red-100">
@@ -23,13 +25,13 @@ export default function FailedBanner({ failedTests, estimatedMinutes, onDecision
</svg>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">
<span className="font-semibold">{failedTests.length} test{failedTests.length !== 1 ? 's' : ''}</span>
{' '}failed last night and require a rerun {' '}
<span className="font-mono text-red-200">{failedTests.join(', ')}</span>
<span className="font-semibold">{rerunTests.length} test{rerunTests.length !== 1 ? 's' : ''}</span>
{' '}require a rerun not completed in last overnight window:{' '}
<span className="font-mono text-red-200">{testIds}</span>
</p>
<p className="text-sm text-red-300 mt-0.5">
Estimated rerun time: <span className="font-semibold text-red-100">{timeStr}</span>
{' '} Rerun these tests during the day today?
{' '} Schedule rerun during daytime today?
</p>
</div>
<div className="flex gap-2 shrink-0">
+14
View File
@@ -14,6 +14,8 @@ function formatCompletionDate(value) {
export default function RightPanel({
completionDate,
startDateOverride,
onStartDateOverrideChange,
daytimeEnabled,
onDaytimeEnabledChange,
topPriority,
@@ -75,6 +77,18 @@ export default function RightPanel({
/>
</div>
<div>
<label className="block text-[11px] font-semibold uppercase tracking-wider text-gray-500 mb-1.5">
Start Date Override
</label>
<input
type="date"
value={startDateOverride}
onChange={(e) => onStartDateOverrideChange(e.target.value)}
className="w-full bg-gray-900 border border-gray-600 rounded-md px-2.5 py-1.5 text-xs text-gray-200 focus:outline-none focus:border-blue-500 font-mono"
/>
</div>
{/* Remake schedule */}
<button
onClick={onRemakeSchedule}
+88 -24
View File
@@ -1,10 +1,11 @@
import { useState, useEffect } from 'react'
function Field({ label, hint, children }) {
function Field({ label, hint, required = false, children }) {
return (
<div>
<label className="block text-xs font-semibold text-gray-300 mb-1">
{label}
{required && <span className="ml-1 text-red-400">*</span>}
{hint && <span className="ml-1.5 text-gray-500 font-normal">{hint}</span>}
</label>
{children}
@@ -15,8 +16,6 @@ function Field({ label, hint, children }) {
const INPUT_CLS =
'w-full bg-gray-900 border border-gray-600 rounded-md px-3 py-1.5 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-blue-500 font-mono'
const TEXTAREA_CLS = INPUT_CLS + ' resize-none'
export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
const [form, setForm] = useState({ ...settings })
@@ -69,7 +68,7 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
File Paths
</p>
<div className="flex flex-col gap-3">
<Field label="P2P / COE CSV">
<Field label="P2P / COE CSV" required>
<input
type="text"
className={INPUT_CLS}
@@ -78,7 +77,7 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
onChange={(e) => set('p2pCoeCsvPath', e.target.value)}
/>
</Field>
<Field label="P3P CSV">
<Field label="P3P CSV" required>
<input
type="text"
className={INPUT_CLS}
@@ -87,7 +86,7 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
onChange={(e) => set('p3pCsvPath', e.target.value)}
/>
</Field>
<Field label="REF Result Directory">
<Field label="REF Result Directory" required>
<input
type="text"
className={INPUT_CLS}
@@ -96,7 +95,7 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
onChange={(e) => set('refResultDir', e.target.value)}
/>
</Field>
<Field label="DUT Result Directory">
<Field label="DUT Result Directory" required>
<input
type="text"
className={INPUT_CLS}
@@ -110,6 +109,88 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
<hr className="border-gray-700" />
{/* Section: Network Shares */}
<div>
<p className="text-[11px] font-bold uppercase tracking-widest text-gray-500 mb-3">
Network Shares
</p>
<div className="flex flex-col gap-3">
<Field label="SMB Username">
<input
type="text"
className={INPUT_CLS}
placeholder="username"
value={form.smbUsername ?? ''}
onChange={(e) => set('smbUsername', e.target.value)}
/>
</Field>
<Field label="SMB Password">
<input
type="password"
className={INPUT_CLS}
placeholder="password"
value={form.smbPassword ?? ''}
onChange={(e) => set('smbPassword', e.target.value)}
/>
</Field>
<Field label="SMB Domain">
<input
type="text"
className={INPUT_CLS}
placeholder="WORKGROUP or DOMAIN"
value={form.smbDomain ?? ''}
onChange={(e) => set('smbDomain', e.target.value)}
/>
</Field>
</div>
</div>
<hr className="border-gray-700" />
{/* Section: Runtime Overrides */}
<div>
<p className="text-[11px] font-bold uppercase tracking-widest text-gray-500 mb-3">
Runtime Overrides
</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<Field label="P2P Minutes">
<input
type="number"
min="1"
step="1"
className={INPUT_CLS}
placeholder="80"
value={form.p2pRuntimeMinutes ?? ''}
onChange={(e) => set('p2pRuntimeMinutes', e.target.value)}
/>
</Field>
<Field label="COE Minutes">
<input
type="number"
min="1"
step="1"
className={INPUT_CLS}
placeholder="115"
value={form.coeRuntimeMinutes ?? ''}
onChange={(e) => set('coeRuntimeMinutes', e.target.value)}
/>
</Field>
<Field label="P3P Minutes">
<input
type="number"
min="1"
step="1"
className={INPUT_CLS}
placeholder="105"
value={form.p3pRuntimeMinutes ?? ''}
onChange={(e) => set('p3pRuntimeMinutes', e.target.value)}
/>
</Field>
</div>
</div>
<hr className="border-gray-700" />
{/* Section: Test Exclusion */}
<div>
<p className="text-[11px] font-bold uppercase tracking-widest text-gray-500 mb-3">
@@ -143,23 +224,6 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
/>
</Field>
</div>
<hr className="border-gray-700" />
{/* Section: Schedule */}
<div>
<p className="text-[11px] font-bold uppercase tracking-widest text-gray-500 mb-3">
Schedule
</p>
<Field label="Start Date Override" hint="(YYYY-MM-DD, leave blank for today)">
<input
type="date"
className={INPUT_CLS}
value={form.startDateOverride ?? ''}
onChange={(e) => set('startDateOverride', e.target.value)}
/>
</Field>
</div>
</div>
{/* Footer */}
+1 -2
View File
@@ -1,8 +1,7 @@
const STATUS_STYLES = {
pending: 'bg-gray-600/60 text-gray-200 border-gray-500',
completed: 'bg-green-800/60 text-green-200 border-green-600',
failed: 'bg-red-800/60 text-red-200 border-red-600',
invalid: 'bg-yellow-800/60 text-yellow-200 border-yellow-600',
rerun: 'bg-red-800/60 text-red-200 border-red-600',
}
function formatConfig(config) {