Settings: refresh interval as value + unit (min/hr) (issue 5)
e54e9567ab7730b34fa6405b45431924db865158
humdrum-tiv <45084903+humdrum-tiv@users.noreply.github.com> · 2026-06-09 12:32
parent aa2ed843
Settings: refresh interval as value + unit (min/hr) (issue 5) Replace the raw-seconds number input with RefreshControl: a number plus a min/hr unit dropdown. The store still holds refreshSeconds — convert seconds → friendliest unit on load (whole hours as hr, else min) and value+unit → seconds on save. Switching unit preserves the real duration. Editing floors at 15 min / 1 hr; existing sub-15-min values display truthfully until edited (no silent bumping). Closes the last open item in ISSUES.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 files changed
ISSUES.md +8 −6
@@ -48,13 +48,15 @@ - Added a one-line hint under the Sources list explaining this.
- `components/SettingsView.tsx`. (`addSource`/`removeSource` handlers dropped; the
`POST /api/sources` + `DELETE /api/sources/[id]` routes remain but are now unused by the UI.)
-## 5. Refresh interval — minutes/hours, not just seconds
+## 5. Refresh interval — minutes/hours, not just seconds ✅ done 2026-06-09
-The per-source refresh control is raw seconds. Want a value + unit (minutes / hours).
-- `components/SettingsView.tsx` → `SourceRow` (the `refreshSeconds` number input).
-- Store still keeps `refreshSeconds`; convert in the UI (e.g. number + unit dropdown →
- seconds on save, seconds → friendliest unit on load).
+- ✅ New `RefreshControl` (`components/SettingsView.tsx`): number + unit (min / hr) dropdown.
+- ✅ Store still keeps raw `refreshSeconds`; converts seconds → friendliest unit on load
+ (whole hours show as `hr`, else `min`) and value+unit → seconds on save.
+- ✅ Switching unit preserves the real duration (rounded into the new unit).
+- ✅ Editing floors at 15 min / 1 hr, but existing sub-15-min values display truthfully
+ until the user actually edits them (no silent bumping).
---
-_Logged 2026-05-21. Fix as time allows._
+_Logged 2026-05-21. All five resolved by 2026-06-09._
components/SettingsView.tsx +63 −12
@@ -272,18 +272,10 @@ <span className="flex-1 text-sm font-medium">{source.label}</span>
<span className="text-xs" style={{ color: "var(--text-faint)" }}>
{state.hasModule ? (state.configured ? "ready" : "needs config") : "local"}
</span>
- <label className="text-xs" style={{ color: "var(--text-muted)" }}>
- every
- <input
- type="number"
- min={30}
- defaultValue={source.refreshSeconds}
- onBlur={(e) => onPatch({ refreshSeconds: Number(e.target.value) })}
- className="w-16 mx-1 px-1 py-0.5 text-xs"
- style={inputStyle}
- />
- s
- </label>
+ <RefreshControl
+ seconds={source.refreshSeconds}
+ onChange={(s) => onPatch({ refreshSeconds: s })}
+ />
<select
value={source.size}
onChange={(e) => onPatch({ size: e.target.value })}
@@ -316,6 +308,65 @@ )}
</div>
{open && configurable && <SourceConfig state={state} onPatch={onPatch} />}
</div>
+ );
+}
+
+// Refresh interval as a friendly value + unit (minutes / hours). The store still
+// holds raw refreshSeconds; we convert on load and on save. Floors at 15 min / 1 hr.
+type RefreshUnit = "min" | "hr";
+
+function RefreshControl({
+ seconds,
+ onChange,
+}: {
+ seconds: number;
+ onChange: (seconds: number) => void;
+}) {
+ // A whole number of hours shows as hours; everything else as minutes.
+ const derivedUnit: RefreshUnit = seconds % 3600 === 0 && seconds >= 3600 ? "hr" : "min";
+ const [unit, setUnit] = useState<RefreshUnit>(derivedUnit);
+
+ const floor = (u: RefreshUnit) => (u === "hr" ? 1 : 15);
+ // Display the true stored value (never lie about it); only the floor below
+ // applies when the user actually edits/commits a new interval.
+ const toValue = (s: number, u: RefreshUnit) =>
+ Math.max(1, Math.round(u === "hr" ? s / 3600 : s / 60));
+ const toSeconds = (v: number, u: RefreshUnit) =>
+ Math.max(floor(u), Math.round(v || 0)) * (u === "hr" ? 3600 : 60);
+
+ const [draft, setDraft] = useState(String(toValue(seconds, unit)));
+
+ // Re-sync the draft whenever the stored value or unit changes externally.
+ useEffect(() => setDraft(String(toValue(seconds, unit))), [seconds, unit]);
+
+ function changeUnit(u: RefreshUnit) {
+ setUnit(u); // keep the same real duration, re-expressed in the new unit
+ onChange(toSeconds(toValue(seconds, u), u));
+ }
+
+ return (
+ <label className="text-xs flex items-center gap-1" style={{ color: "var(--text-muted)" }}>
+ every
+ <input
+ type="number"
+ min={floor(unit)}
+ value={draft}
+ onChange={(e) => setDraft(e.target.value)}
+ onBlur={() => onChange(toSeconds(Number(draft), unit))}
+ className="w-14 px-1 py-0.5 text-xs"
+ style={inputStyle}
+ />
+ <select
+ value={unit}
+ onChange={(e) => changeUnit(e.target.value as RefreshUnit)}
+ className="text-xs px-1 py-0.5"
+ style={inputStyle}
+ aria-label="Refresh unit"
+ >
+ <option value="min">min</option>
+ <option value="hr">hr</option>
+ </select>
+ </label>
);
}