diff --git a/app/(app)/device/[friendly_id]/client-page.tsx b/app/(app)/device/[friendly_id]/client-page.tsx index 6b464b70..4c1dfc22 100644 --- a/app/(app)/device/[friendly_id]/client-page.tsx +++ b/app/(app)/device/[friendly_id]/client-page.tsx @@ -125,7 +125,11 @@ export default function DeviceClientPage({ ...editedDevice, [parent]: { ...(editedDevice[parent as keyof Device] as Record), - [child]: value, + // Inputs always give strings; the schedule stores seconds as a number + [child]: + name === "refresh_schedule.default_refresh_rate" + ? Number.parseInt(value, 10) + : value, }, }); } else { diff --git a/lib/device/defaults.test.ts b/lib/device/defaults.test.ts new file mode 100644 index 00000000..cb272abe --- /dev/null +++ b/lib/device/defaults.test.ts @@ -0,0 +1,69 @@ +/// +import { normalizeRefreshSchedule } from "@/lib/device/defaults"; + +describe("normalizeRefreshSchedule", () => { + it("keeps numeric schedules as they are", () => { + expect( + normalizeRefreshSchedule({ + default_refresh_rate: 600, + time_ranges: [ + { start_time: "23:30", end_time: "06:30", refresh_rate: 6000 }, + ], + }), + ).toEqual({ + default_refresh_rate: 600, + time_ranges: [ + { start_time: "23:30", end_time: "06:30", refresh_rate: 6000 }, + ], + }); + }); + + it("accepts refresh rates saved as numeric strings", () => { + expect( + normalizeRefreshSchedule({ + default_refresh_rate: "600", + time_ranges: [ + { start_time: "23:30", end_time: "06:30", refresh_rate: "6000" }, + ], + }), + ).toEqual({ + default_refresh_rate: 600, + time_ranges: [ + { start_time: "23:30", end_time: "06:30", refresh_rate: 6000 }, + ], + }); + }); + + it("parses a JSON string column", () => { + expect(normalizeRefreshSchedule('{"default_refresh_rate":"600"}')).toEqual({ + default_refresh_rate: 600, + time_ranges: [], + }); + }); + + it("rejects a non-numeric default refresh rate", () => { + expect(normalizeRefreshSchedule({ default_refresh_rate: "" })).toBeNull(); + expect( + normalizeRefreshSchedule({ default_refresh_rate: "abc" }), + ).toBeNull(); + expect(normalizeRefreshSchedule({ default_refresh_rate: null })).toBeNull(); + }); + + it("drops invalid time ranges but keeps the rest of the schedule", () => { + expect( + normalizeRefreshSchedule({ + default_refresh_rate: 600, + time_ranges: [ + { start_time: "23:30", end_time: "06:30", refresh_rate: "abc" }, + { start_time: "12:00", end_time: "13:00", refresh_rate: 60 }, + null, + ], + }), + ).toEqual({ + default_refresh_rate: 600, + time_ranges: [ + { start_time: "12:00", end_time: "13:00", refresh_rate: 60 }, + ], + }); + }); +}); diff --git a/lib/device/defaults.ts b/lib/device/defaults.ts index 64aee308..483e0984 100644 --- a/lib/device/defaults.ts +++ b/lib/device/defaults.ts @@ -28,6 +28,16 @@ export function serializeRefreshSchedule(schedule: RefreshSchedule): string { return JSON.stringify(schedule); } +// Older versions of the device form saved refresh rates as strings ("600"), +// so accept numeric strings as well as numbers. +function parseRefreshSeconds(value: unknown): number | null { + if (typeof value === "number") return Number.isFinite(value) ? value : null; + if (typeof value === "string" && /^\s*\d+\s*$/.test(value)) { + return Number.parseInt(value, 10); + } + return null; +} + export function normalizeRefreshSchedule( value: unknown, ): RefreshSchedule | null { @@ -40,20 +50,32 @@ export function normalizeRefreshSchedule( } } if (typeof value !== "object" || Array.isArray(value)) return null; - const candidate = value as Partial; - if (typeof candidate.default_refresh_rate !== "number") return null; - const timeRanges = Array.isArray(candidate.time_ranges) - ? candidate.time_ranges.filter( - (range): range is RefreshSchedule["time_ranges"][number] => - typeof range === "object" && - range !== null && - typeof range.start_time === "string" && - typeof range.end_time === "string" && - typeof range.refresh_rate === "number", - ) - : []; + const candidate = value as Record; + const defaultRefreshRate = parseRefreshSeconds( + candidate.default_refresh_rate, + ); + if (defaultRefreshRate === null) return null; + const timeRanges: RefreshSchedule["time_ranges"] = []; + if (Array.isArray(candidate.time_ranges)) { + for (const range of candidate.time_ranges) { + if (typeof range !== "object" || range === null) continue; + const { start_time, end_time, refresh_rate } = range as Record< + string, + unknown + >; + const refreshRate = parseRefreshSeconds(refresh_rate); + if ( + typeof start_time !== "string" || + typeof end_time !== "string" || + refreshRate === null + ) { + continue; + } + timeRanges.push({ start_time, end_time, refresh_rate: refreshRate }); + } + } return { - default_refresh_rate: candidate.default_refresh_rate, + default_refresh_rate: defaultRefreshRate, time_ranges: timeRanges, }; }