"use client"; /** * ForecastMetaLine — compact freshness / traceability footer for Section 6 (§22). * * Surfaces the report `meta` provenance that, until now, lived only in the * exports («Сформировано» row): WHEN the forecast was computed, over WHICH * horizons, and the schema version. For a forecasting tool «прогноз рассчитан * <дата>» is a trust/staleness signal — without it the horizons appear only * implicitly via the chart axis and the compute date is invisible. * * Three independently-guarded segments joined by a middot, caption typography, * muted tokens (schema is the MOST muted — traceability, not headline): * * Прогноз рассчитан: 7 июня 2026 · горизонты 6 / 12 / 18 / 24 мес · схема 1.0 * * Date source: the run's persist time (`runCreatedAt` — the forecast envelope's * `created_at`, always set for a persisted run) is preferred; `meta.generated_at` * is the fallback. The PURE assembler / orchestrator deliberately do NOT stamp * `generated_at` (determinism), so it is frequently null in real runs — but the * persisted run always carries `created_at`, which is the real «когда рассчитан». * * GRACEFUL: renders null when every segment is empty (e.g. 202-pending or a thin * report). When neither date source resolves, only that segment is skipped while * horizons + schema still render. An invalid/unparseable date is treated as * missing. A date-only ISO string («2026-06-03») renders date-only; a datetime * with a meaningful (non-midnight) time appends «, ЧЧ:ММ» — a real run timestamp * carries a genuine time, so it shows «7 июня 2026, 14:20» (precise compute time). */ import type { ReportMeta } from "@/types/forecast"; /** «6 / 12 / 18 / 24 мес» — horizons joined; "" when the list is empty. */ function formatHorizons(horizons: number[]): string { if (horizons.length === 0) return ""; return `${horizons.join(" / ")} мес`; } /** * «7 июня 2026» (date-only) or «7 июня 2026, 10:30» (meaningful time) — RU long * date from an ISO 8601 string. Returns "" for null/empty/unparseable input. * * A bare date («2026-06-03») parses to local midnight, so a 00:00 time is treated * as «no time» to avoid a misleading «, 00:00». A datetime carrying a real time * (and an explicit time component in the source string) appends «, ЧЧ:ММ». */ function formatComputedAt(computedAt: string | null): string { if (computedAt == null || computedAt.trim() === "") return ""; const ts = Date.parse(computedAt); if (Number.isNaN(ts)) return ""; const date = new Date(ts); const datePart = date.toLocaleDateString("ru", { day: "numeric", month: "long", year: "numeric", }); const hasTimeComponent = computedAt.includes("T"); const isMidnight = date.getHours() === 0 && date.getMinutes() === 0; if (!hasTimeComponent || isMidnight) return datePart; const timePart = date.toLocaleTimeString("ru", { hour: "2-digit", minute: "2-digit", }); return `${datePart}, ${timePart}`; } const SEP_STYLE: React.CSSProperties = { color: "var(--fg-tertiary)", // hair spaces around the middot for RU typographic breathing room. padding: "0 2px", }; export function ForecastMetaLine({ meta, runCreatedAt, }: { meta: ReportMeta; /** * The forecast envelope's `created_at` (the run's persist time) — the real * «когда рассчитан» signal, always set for a persisted run. Preferred over the * frequently-null `meta.generated_at`. */ runCreatedAt?: string | null; }) { // Run persist time wins; the (often null) deterministic meta stamp is fallback. const computedAt = formatComputedAt(runCreatedAt ?? meta.generated_at); const horizons = formatHorizons(meta.horizons); const schemaVersion = meta.schema_version?.trim() ?? ""; const segments: React.ReactNode[] = []; if (computedAt) { segments.push( Прогноз рассчитан: {computedAt} , ); } if (horizons) { segments.push( горизонты {horizons} , ); } if (schemaVersion) { segments.push( схема {schemaVersion} , ); } if (segments.length === 0) return null; return (

{segments.map((segment, i) => ( {i > 0 && ( · )} {segment} ))}

); }