Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/(app)/device/[friendly_id]/client-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,11 @@ export default function DeviceClientPage({
...editedDevice,
[parent]: {
...(editedDevice[parent as keyof Device] as Record<string, unknown>),
[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 {
Expand Down
69 changes: 69 additions & 0 deletions lib/device/defaults.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/// <reference types="jest" />
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 },
],
});
});
});
48 changes: 35 additions & 13 deletions lib/device/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -40,20 +50,32 @@ export function normalizeRefreshSchedule(
}
}
if (typeof value !== "object" || Array.isArray(value)) return null;
const candidate = value as Partial<RefreshSchedule>;
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<string, unknown>;
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,
};
}