feat(validation): bound entry timestamps with a shared plausibility guard

Only medication intake bounded its capture instant — a hand-rolled POST or a
faulty import could backdate a measurement to epoch zero or forward-date a
mood entry into next year, and both sailed through validation.

Extract the intake bound into a shared `validateEntryInstant` helper: reject
any instant more than a five-minute clock-skew tolerance in the future, and
reject anything before a 1900 far-past floor. Callers can clamp the past
window tighter — medication intake keeps its five-year floor — and override
the past-bound message to preserve an established contract.

Apply it to the measurement create/edit schema (`measuredAt`), the mood
schema (`moodLoggedAt`), and the cycle period + day-log (`loggedAt`);
medication intake now delegates to the same helper. The capture forms gain a
matching `max` on the datetime picker so a future instant can't be selected
in the UI either. The wire shape is unchanged, so the OpenAPI contract holds.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 13:51:51 +02:00
1 parent e9138bb4f6
commit 3e7aa49eb0
12 files changed
+323 -23

No files matched your search

@@ -472,6 +472,10 @@ export function MeasurementForm({
id="measuredAt"
value={measuredAt}
onChange={(e) => setMeasuredAt(e.target.value)}
// v1.17 W1b — mirror the server-side plausibility bound: the
// picker cannot select a future instant. Matches the
// `validateEntryInstant` refine on `measuredAt`.
max={getDefaultMeasuredAtValue()}
required
aria-required="true"
aria-invalid={!!error || undefined}
@@ -1273,6 +1273,8 @@ export function MeasurementList({
id="edit-measuredAt"
value={editMeasuredAt}
onChange={(e) => setEditMeasuredAt(e.target.value)}
// v1.17 W1b — match the server bound: no future instant.
max={toDateTimeLocalValue(new Date().toISOString())}
required
aria-required="true"
aria-invalid={!!editError || undefined}
+2
View File
@@ -328,6 +328,8 @@ export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
id="mood-logged-at"
value={moodLoggedAt}
onChange={(e) => setMoodLoggedAt(e.target.value)}
// v1.17 W1b — match the server bound: no future instant.
max={getDefaultMoodLoggedAtValue()}
required
aria-required="true"
aria-invalid={!!error || undefined}
+2
View File
@@ -842,6 +842,8 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
id="edit-mood-logged-at"
value={editMoodLoggedAt}
onChange={(e) => setEditMoodLoggedAt(e.target.value)}
// v1.17 W1b — match the server bound: no future instant.
max={toDateTimeLocalValue(new Date().toISOString())}
required
/>
</div>