diff --git a/ordinis-admin-ui/src/components/timetravel/TimeTravelModal.tsx b/ordinis-admin-ui/src/components/timetravel/TimeTravelModal.tsx index 5587ac7..86547ba 100644 --- a/ordinis-admin-ui/src/components/timetravel/TimeTravelModal.tsx +++ b/ordinis-admin-ui/src/components/timetravel/TimeTravelModal.tsx @@ -109,25 +109,22 @@ export function TimeTravelModal({ isAtNow0 ? undefined : { at: new Date(picked).toISOString() }, ) - // Range: от min(marks, createdAt) до max(now+30d, future marks)+padding. + // Range: derived purely from real data — marks (record validFroms, + // включая upcoming scheduled, передаются parent'ом), createdAt, now. + // No arbitrary future window: max берётся max(marks) — last real change + // point. User feedback: «крайняя дата должна быть исходя из реальной в + // этом словаре». // - // Future support (fix paired с TimeTravelPicker !220): если у dict есть - // scheduled records (validFrom > now) или user navigated к future date, - // slider должен дотягиваться. Раньше max = now + 1day pad — record с - // validFrom=18.05 был недостижим если сейчас 15.05. - // - // Default future window = 30 days, match'ит TimeTravelPicker.tsx logic. + // Если все marks в прошлом — max=now (user всё равно может перейти к now). + // Если есть scheduled validFrom > now — max тянется до furthest scheduled. const createdMs = Date.parse(detail.createdAt) const range = useMemo(() => { const DAY_MS = 86_400_000 - const FUTURE_WINDOW_MS = 30 * DAY_MS const minMark = marks.length > 0 ? Math.min(...marks, createdMs) : createdMs - DAY_MS - // Push max forward: at least now + 30 days, и further если есть future marks. const maxMark = marks.length > 0 ? Math.max(...marks, now) : now - const effectiveMax = Math.max(maxMark, now + FUTURE_WINDOW_MS) - const span = effectiveMax - minMark + const span = maxMark - minMark const pad = Math.max(span * 0.04, DAY_MS) - return { min: minMark - pad, max: effectiveMax + pad } + return { min: minMark - pad, max: maxMark + pad } }, [marks, createdMs, now]) // Sync picked when value prop changes while modal open. diff --git a/ordinis-admin-ui/src/components/timetravel/TimeTravelPicker.tsx b/ordinis-admin-ui/src/components/timetravel/TimeTravelPicker.tsx index 123bbfc..e065797 100644 --- a/ordinis-admin-ui/src/components/timetravel/TimeTravelPicker.tsx +++ b/ordinis-admin-ui/src/components/timetravel/TimeTravelPicker.tsx @@ -39,20 +39,18 @@ export function TimeTravelPicker({ return Number.isNaN(d.getTime()) ? undefined : d }, [value]) - // Range: from min(marks, value) − padding до max(now, future marks) + padding. + // Range: derived purely from real data — marks (record validFrom timestamps + // включая upcoming scheduled), createdAt, и picked value. Никаких arbitrary + // future windows: если в словаре нет записей на 18.05 — slider туда не + // тянется. User feedback: «крайняя дата должна быть исходя из реальной в + // этом словаре». // - // Future support: если у dict'а есть records с validFrom > now (scheduled), - // или user navigated к future date (asOf=18.05 для preview new record), - // slider должен дотягиваться до этой даты. Раньше max = now + 1day pad — - // scheduled records на дату дальше 1 день были недоступны через slider - // (user не мог время-travel'ить чтобы их увидеть). - // - // Default future window = 30 days, чтобы планирование на месяц вперёд - // (типичный business horizon для NSI updates) было без ручного URL hack. + // Каноничный max берётся max(marks) — это последняя «интересная точка» + // (либо last existing record validFrom, либо furthest scheduled validFrom). + // Если marks пусты — fallback на now + 1 day pad чтобы slider не вырождался. const range = useMemo(() => { const now = Date.now() const DAY_MS = 86_400_000 - const FUTURE_WINDOW_MS = 30 * DAY_MS let minMark = marks.length > 0 ? Math.min(...marks) : now - 365 * DAY_MS let maxMark = marks.length > 0 ? Math.max(...marks) : now @@ -60,8 +58,9 @@ export function TimeTravelPicker({ minMark = Math.min(minMark, valueDate.getTime()) maxMark = Math.max(maxMark, valueDate.getTime()) } - // Push max forward: at least now + 30 days, и further если есть будущие marks. - const effectiveMax = Math.max(maxMark, now + FUTURE_WINDOW_MS) + // Ensure max ≥ now чтобы «сейчас» всегда был достижим, even если все + // marks в прошлом (закрытый dict). + const effectiveMax = Math.max(maxMark, now) const span = effectiveMax - minMark const pad = Math.max(span * 0.05, DAY_MS) // ≥1 день return { min: minMark - pad, max: effectiveMax + pad }