implemented watcher and rerun logic
This commit is contained in:
+91
-17
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,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}
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
Reference in New Issue
Block a user