showing previous version schedule as faded

This commit is contained in:
2026-07-20 14:16:56 -04:00
parent a49772aece
commit 5eebee70d8
8 changed files with 293 additions and 91 deletions
+113 -1
View File
@@ -119,6 +119,87 @@ function toTonightConfigRows(scheduleData) {
.sort((a, b) => a.sta.localeCompare(b.sta))
}
const SHIFT_KEYS = ['shift1', 'shift2', 'shift3']
const SHIFT_INDEX_BY_KEY = {
shift1: 1,
shift2: 2,
shift3: 3,
}
function createEmptyScheduleDay() {
return { shift1: [], shift2: [], shift3: [] }
}
function getShiftTests(scheduleData, dateKey, shiftKey) {
return scheduleData[dateKey]?.[shiftKey] ?? []
}
function isOnOrAfterScheduleStart(dateKey, shiftKey, scheduleStartShift) {
if (!scheduleStartShift?.date || !scheduleStartShift?.shiftIndex) return false
if (dateKey > scheduleStartShift.date) return true
if (dateKey < scheduleStartShift.date) return false
const currentShiftIndex = SHIFT_INDEX_BY_KEY[shiftKey] ?? 99
return currentShiftIndex >= scheduleStartShift.shiftIndex
}
function collectSnapshotShiftKeys(currentScheduleData = {}, previousScheduleData = {}, scheduleStartShift = null) {
const snapshotShiftKeys = new Set()
const dateKeys = new Set([
...Object.keys(currentScheduleData),
...Object.keys(previousScheduleData),
])
for (const dateKey of dateKeys) {
for (const shiftKey of SHIFT_KEYS) {
if (isOnOrAfterScheduleStart(dateKey, shiftKey, scheduleStartShift)) {
continue
}
const currentTests = getShiftTests(currentScheduleData, dateKey, shiftKey)
const previousTests = getShiftTests(previousScheduleData, dateKey, shiftKey)
// Show historical snapshot only when this shift disappeared in the latest schedule.
if (currentTests.length === 0 && previousTests.length > 0) {
snapshotShiftKeys.add(`${dateKey}::${shiftKey}`)
}
}
}
return snapshotShiftKeys
}
function markHistoricalSnapshot(tests = []) {
return tests.map((test) => ({
...test,
isPreviousVersionSnapshot: true,
}))
}
function buildCalendarScheduleData(currentScheduleData = {}, previousScheduleData = {}, snapshotShiftKeys = new Set()) {
const calendarScheduleData = {}
const dateKeys = new Set([
...Object.keys(currentScheduleData),
...Object.keys(previousScheduleData),
])
for (const dateKey of dateKeys) {
const mergedDay = createEmptyScheduleDay()
for (const shiftKey of SHIFT_KEYS) {
const shiftSnapshotKey = `${dateKey}::${shiftKey}`
mergedDay[shiftKey] = snapshotShiftKeys.has(shiftSnapshotKey)
? markHistoricalSnapshot(getShiftTests(previousScheduleData, dateKey, shiftKey))
: getShiftTests(currentScheduleData, dateKey, shiftKey)
}
calendarScheduleData[dateKey] = mergedDay
}
return calendarScheduleData
}
const DEFAULT_SETTINGS = {
p2pCoeCsvPath: '',
p3pCsvPath: '',
@@ -171,6 +252,8 @@ export default function App() {
const [startDateOverride, setStartDateOverride] = useState('')
const [failedTests, setFailedTests] = useState([])
const [scheduleData, setScheduleData] = useState({})
const [previousScheduleData, setPreviousScheduleData] = useState({})
const [scheduleStartShift, setScheduleStartShift] = useState(null)
const [scheduleWindows, setScheduleWindows] = useState([])
const [selectedWindowId, setSelectedWindowId] = useState(null)
const [completionDate, setCompletionDate] = useState(null)
@@ -178,6 +261,14 @@ export default function App() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const tonightConfigRows = useMemo(() => toTonightConfigRows(scheduleData), [scheduleData])
const snapshotShiftKeys = useMemo(
() => collectSnapshotShiftKeys(scheduleData, previousScheduleData, scheduleStartShift),
[scheduleData, previousScheduleData, scheduleStartShift],
)
const calendarScheduleData = useMemo(
() => buildCalendarScheduleData(scheduleData, previousScheduleData, snapshotShiftKeys),
[scheduleData, previousScheduleData, snapshotShiftKeys],
)
const windowLookup = useMemo(() => {
const lookup = new Map()
@@ -205,7 +296,27 @@ export default function App() {
const fetchSchedule = useCallback(async (start) => {
try {
const data = await api.getScheduleWeek(toKey(start))
let previousVersionScheduleData = {}
if ((data.schedule_version ?? 0) > 1) {
try {
const previousData = await api.getScheduleWeek(toKey(start), data.schedule_version - 1)
previousVersionScheduleData = groupScheduleItems(previousData.items)
} catch (previousError) {
console.warn('Failed to fetch previous schedule version:', previousError)
}
}
setScheduleData(groupScheduleItems(data.items))
setPreviousScheduleData(previousVersionScheduleData)
if (data.schedule_start_date && data.schedule_start_shift_index) {
setScheduleStartShift({
date: data.schedule_start_date,
shiftIndex: data.schedule_start_shift_index,
})
} else {
setScheduleStartShift(null)
}
setScheduleWindows(data.windows ?? [])
setCompletionDate(data.completion_date ?? null)
} catch (e) {
@@ -386,7 +497,7 @@ export default function App() {
<main className="relative flex flex-1 gap-4 p-4 overflow-hidden">
<div className="flex-1 min-w-0 flex flex-col">
<Calendar
scheduleData={scheduleData}
scheduleData={calendarScheduleData}
daytimeDateKey={null}
weekStart={weekStart}
onWeekChange={setWeekStart}
@@ -398,6 +509,7 @@ export default function App() {
}))
}}
windowLookup={windowLookup}
snapshotShiftKeys={snapshotShiftKeys}
onWindowSelect={handleWindowSelect}
/>
</div>
+7 -1
View File
@@ -30,7 +30,13 @@ export const api = {
// Schedule
compileSchedule: (opts) => request('POST', '/schedule/compile', opts),
getScheduleWeek: (start) => request('GET', `/schedule/week?start=${start}`),
getScheduleWeek: (start, version = null) => {
const params = new URLSearchParams({ start })
if (version !== null && version !== undefined) {
params.set('version', String(version))
}
return request('GET', `/schedule/week?${params.toString()}`)
},
getRerunTests: () => request('GET', '/tests/rerun'),
}
+2
View File
@@ -57,6 +57,7 @@ export default function Calendar({
dualDeviceWeekendWeekEnabled = false,
onDualDeviceWeekendWeekEnabledChange,
windowLookup = new Map(),
snapshotShiftKeys = new Set(),
onWindowSelect,
}) {
const DEFAULT_SHIFT_MIN_ROWS = 3
@@ -226,6 +227,7 @@ export default function Calendar({
slotHeights={slotHeights}
showShift2={isWeekend || (daytimeDateKey !== null && key === daytimeDateKey)}
windowLookup={windowLookup}
snapshotShiftKeys={snapshotShiftKeys}
onWindowSelect={onWindowSelect}
/>
)
+7
View File
@@ -13,6 +13,7 @@ export default function DayColumn({
shiftMinRows = { shift2: 1, shift3: 1, nextDayShift1: 1 },
slotHeights = { shift2: 0, shift3: 0, nextDayShift1: 0 },
windowLookup = new Map(),
snapshotShiftKeys = new Set(),
onWindowSelect,
}) {
const dayName = DAY_NAMES[date.getDay()]
@@ -20,11 +21,17 @@ export default function DayColumn({
const dateKey = date.toISOString().slice(0, 10)
const nextDateKey = new Date(date.getTime() + (24 * 60 * 60 * 1000)).toISOString().slice(0, 10)
function isSnapshotShift(snapshotDateKey, shiftIndex) {
return snapshotShiftKeys.has(`${snapshotDateKey}::shift${shiftIndex}`)
}
function getWindowForShift(shiftIndex) {
if (isSnapshotShift(dateKey, shiftIndex)) return null
return windowLookup.get(`${dateKey}::shift${shiftIndex}`) ?? null
}
function getNextDayShift1Window() {
if (isSnapshotShift(nextDateKey, 1)) return null
return windowLookup.get(`${nextDateKey}::shift1`) ?? null
}
+2 -1
View File
@@ -50,6 +50,7 @@ export default function TestCard({ test }) {
const normalizedStatus = String(test?.status ?? 'pending').trim().toLowerCase()
const style = STATUS_STYLES[normalizedStatus] ?? STATUS_STYLES.pending
const deviceAccentClass = getDeviceAccentClass(test.device)
const isPreviousVersionSnapshot = Boolean(test?.isPreviousVersionSnapshot)
const cardRef = useRef(null)
const [tooltipPos, setTooltipPos] = useState({ top: '0px', left: '0px' })
const [isHovered, setIsHovered] = useState(false)
@@ -92,7 +93,7 @@ export default function TestCard({ test }) {
<div ref={cardRef} className="relative">
<div
className={`relative px-1.5 py-0.5 pl-3 rounded border text-xs font-mono truncate cursor-default select-none ${style}`}
style={{ maxWidth: '100%' }}
style={{ maxWidth: '100%', opacity: isPreviousVersionSnapshot ? 0.45 : 1 }}
>
<span className={`absolute inset-y-0 left-0 w-1 rounded-l ${deviceAccentClass}`} aria-hidden="true" />
{test.test_id}