Update scheduling algorithm
This commit is contained in:
@@ -402,6 +402,35 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRestartAndClearData() {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
await api.restartAndClearData()
|
||||
|
||||
setSettings(DEFAULT_SETTINGS)
|
||||
setTopPriorityDut('')
|
||||
setTopPriorityRef('')
|
||||
setLowestPriority('')
|
||||
setStartDateOverride('')
|
||||
setFailedTests([])
|
||||
setScheduleData({})
|
||||
setPreviousScheduleData({})
|
||||
setScheduleStartShift(null)
|
||||
setScheduleWindows([])
|
||||
setSelectedWindowId(null)
|
||||
setCompletionDate(null)
|
||||
|
||||
await fetchSchedule(weekStart)
|
||||
await fetchRerunTests()
|
||||
} catch (e) {
|
||||
setError(e.message)
|
||||
throw e
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemakeSchedule() {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
@@ -541,6 +570,7 @@ export default function App() {
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
settings={settings}
|
||||
onSave={handleSaveSettings}
|
||||
onRestart={handleRestartAndClearData}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ export const api = {
|
||||
// Settings
|
||||
getSettings: () => request('GET', '/settings'),
|
||||
saveSettings: (settings) => request('POST', '/settings', { settings }),
|
||||
restartAndClearData: () => request('POST', '/settings/restart'),
|
||||
|
||||
// Holidays
|
||||
getHolidays: () => request('GET', '/holidays'),
|
||||
|
||||
@@ -5,7 +5,17 @@ export default function FailedBanner({ rerunTests, onDecision }) {
|
||||
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(', ')
|
||||
|
||||
// Group test IDs by device
|
||||
const byDevice = rerunTests.reduce((acc, t) => {
|
||||
const device = t.device ?? 'Unknown'
|
||||
if (!acc[device]) acc[device] = []
|
||||
acc[device].push(t.test_id)
|
||||
return acc
|
||||
}, {})
|
||||
const deviceSummaries = Object.entries(byDevice)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([device, ids]) => `${device}: ${ids.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">
|
||||
@@ -26,9 +36,13 @@ export default function FailedBanner({ rerunTests, onDecision }) {
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium">
|
||||
<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>
|
||||
{' '}require a rerun — not completed in last overnight window:
|
||||
</p>
|
||||
{deviceSummaries.map((summary) => (
|
||||
<p key={summary} className="text-sm font-mono text-red-200 mt-0.5 truncate">
|
||||
{summary}
|
||||
</p>
|
||||
))}
|
||||
<p className="text-sm text-red-300 mt-0.5">
|
||||
Estimated rerun time: <span className="font-semibold text-red-100">{timeStr}</span>
|
||||
{' '}— Schedule rerun during daytime today?
|
||||
|
||||
@@ -16,12 +16,22 @@ function Field({ label, hint, required = false, 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'
|
||||
|
||||
export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
|
||||
export default function SettingsModal({ isOpen, onClose, settings, onSave, onRestart }) {
|
||||
const [form, setForm] = useState({ ...settings })
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
const [confirmText, setConfirmText] = useState('')
|
||||
const [restartBusy, setRestartBusy] = useState(false)
|
||||
const [restartError, setRestartError] = useState(null)
|
||||
|
||||
// Sync if parent settings change while modal is closed
|
||||
useEffect(() => {
|
||||
if (!isOpen) setForm({ ...settings })
|
||||
if (!isOpen) {
|
||||
setForm({ ...settings })
|
||||
setConfirmOpen(false)
|
||||
setConfirmText('')
|
||||
setRestartBusy(false)
|
||||
setRestartError(null)
|
||||
}
|
||||
}, [isOpen, settings])
|
||||
|
||||
if (!isOpen) return null
|
||||
@@ -35,6 +45,30 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
|
||||
onClose()
|
||||
}
|
||||
|
||||
async function handleRestartClick() {
|
||||
if (!confirmOpen) {
|
||||
setConfirmOpen(true)
|
||||
setRestartError(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (confirmText.trim() !== 'RESET') {
|
||||
setRestartError('Type RESET to confirm database clear.')
|
||||
return
|
||||
}
|
||||
|
||||
setRestartBusy(true)
|
||||
setRestartError(null)
|
||||
try {
|
||||
await onRestart()
|
||||
onClose()
|
||||
} catch (err) {
|
||||
setRestartError(err?.message || 'Failed to restart and clear data.')
|
||||
} finally {
|
||||
setRestartBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||
@@ -263,6 +297,63 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<hr className="border-gray-700" />
|
||||
|
||||
{/* Section: Danger Zone */}
|
||||
<div>
|
||||
<p className="text-[11px] font-bold uppercase tracking-widest text-red-400 mb-3">
|
||||
Danger Zone
|
||||
</p>
|
||||
<div className="rounded-lg border border-red-800 bg-red-950/30 p-3">
|
||||
<p className="text-sm text-red-200 font-semibold">Reset And Clear Database</p>
|
||||
<p className="mt-1 text-xs text-red-300/90">
|
||||
This permanently deletes tests, schedules, settings, and holidays from the database.
|
||||
</p>
|
||||
|
||||
{confirmOpen && (
|
||||
<div className="mt-3 flex flex-col gap-2">
|
||||
<p className="text-xs text-red-200">
|
||||
Type <span className="font-semibold">RESET</span> below, then click the button again to confirm.
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
className={INPUT_CLS}
|
||||
placeholder="Type RESET"
|
||||
value={confirmText}
|
||||
onChange={(e) => setConfirmText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{restartError && (
|
||||
<p className="mt-2 text-xs text-red-300">{restartError}</p>
|
||||
)}
|
||||
|
||||
<div className="mt-3 flex gap-2">
|
||||
<button
|
||||
onClick={handleRestartClick}
|
||||
disabled={restartBusy}
|
||||
className="px-3 py-1.5 text-sm font-semibold text-white bg-red-700 hover:bg-red-600 border border-red-500 rounded-md transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
{restartBusy ? 'Clearing...' : confirmOpen ? 'Confirm Clear DB' : 'Reset'}
|
||||
</button>
|
||||
{confirmOpen && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setConfirmOpen(false)
|
||||
setConfirmText('')
|
||||
setRestartError(null)
|
||||
}}
|
||||
disabled={restartBusy}
|
||||
className="px-3 py-1.5 text-sm text-gray-300 bg-gray-800 border border-gray-600 rounded-md hover:bg-gray-700 transition-colors disabled:opacity-60"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
|
||||
Reference in New Issue
Block a user