fix(frontend): fix off-by-one date shown in the backfill horizon picker
daysAgoToISODate built the date at local midnight but formatted it with toISOString(), which converts to UTC first -- in any timezone ahead of UTC (e.g. Europe/Paris), local midnight is still the previous day in UTC, so the picker displayed a date one day earlier than the one that was actually selected/stored. Format from the Date's local year/month/day components instead of going through UTC.
This commit is contained in:
@@ -31,6 +31,54 @@ function NumberField({
|
||||
);
|
||||
}
|
||||
|
||||
// Renders/edits BackfillHorizonDays as a calendar date (the day sync should
|
||||
// backfill through) rather than a raw day count, which is easier to reason
|
||||
// about than counting days back -- the stored value is still a day count
|
||||
// (what the backend actually consumes), just presented as its equivalent
|
||||
// date, recomputed against "today" on every render/change.
|
||||
function daysAgoToISODate(days: number): string {
|
||||
const d = new Date();
|
||||
d.setHours(0, 0, 0, 0);
|
||||
d.setDate(d.getDate() - days);
|
||||
// Deliberately not toISOString(): that formats in UTC, which in any
|
||||
// timezone ahead of UTC shifts local midnight back to the previous day.
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
function isoDateToDaysAgo(iso: string): number {
|
||||
const picked = new Date(iso + "T00:00:00");
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const diffMs = today.getTime() - picked.getTime();
|
||||
return Math.max(0, Math.round(diffMs / (24 * 60 * 60 * 1000)));
|
||||
}
|
||||
|
||||
function BackfillHorizonField({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: number;
|
||||
onChange: (v: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<label>
|
||||
Past sync (days)
|
||||
<input
|
||||
type="date"
|
||||
value={daysAgoToISODate(value)}
|
||||
max={daysAgoToISODate(0)}
|
||||
onChange={(e) => {
|
||||
if (e.target.value) onChange(isoDateToDaysAgo(e.target.value));
|
||||
}}
|
||||
/>
|
||||
<span className="field-hint">{value} day{value === 1 ? "" : "s"} ago</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
// How long to wait after the last edit before actually saving, so typing a
|
||||
// name or a multi-digit number doesn't fire one request per keystroke --
|
||||
// only the last edit in a burst triggers a save, covering every field
|
||||
@@ -162,8 +210,7 @@ export function Profile({ onSaved }: { onSaved?: (p: ProfileType) => void }) {
|
||||
onChange={(e) => set("GarminPassword", e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<NumberField
|
||||
label="Past sync (days)"
|
||||
<BackfillHorizonField
|
||||
value={profile.BackfillHorizonDays}
|
||||
onChange={(v) => set("BackfillHorizonDays", v)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user