/* ============================================================ Panels.jsx — header + left panel (ESM) Exports: Header, SidePanel, STATUS_META, fmtDur. ============================================================ */ import React from "react"; import { COL } from "./data.js"; const STATUS_META = { operational: { c: "#5cae7e", t: "НА ОРБИТЕ" }, capture: { c: "#d99a4e", t: "СЪЁМКА" }, downlink: { c: "#5b9bd5", t: "СБРОС" }, prohibit: { c: "#d9685f", t: "ЗАПРЕТ" }, }; // Время в проекте — локальное настенное (см. память project_tgu_ui_utc): часы, планы // и flight-line идут в зоне браузера без UTC-семантики. Поэтому local-геттеры, не UTC. function fmtClock(ms) { const d = new Date(ms); const p = (n) => String(n).padStart(2, "0"); return { time: `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`, date: `${p(d.getDate())}.${p(d.getMonth() + 1)}.${d.getFullYear()}`, dow: ["ВС", "ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ"][d.getDay()], }; } function fmtDur(ms) { const s = Math.max(0, Math.round(ms / 1000)); const m = Math.floor(s / 60), ss = s % 60; return `${m}:${String(ss).padStart(2, "0")}`; } function Header({ simTime, playing, speed, live, onPlay, onSpeed, onNow, onStep }) { const ck = fmtClock(simTime); const speeds = [1, 10, 60, 600]; return (
СОСТОЯНИЕ ГРУППИРОВКИ
MISSION OPS · ТГУ
{speeds.map((s) => ( ))}
{live ? "LIVE" : "СИМ"}
{ck.time} местн.
{ck.dow} · {ck.date}
); } function SummaryTiles({ counts }) { const tiles = [ { k: "capture", label: "СНИМАЮТ", n: counts.capture, c: STATUS_META.capture.c }, { k: "downlink", label: "СБРОС", n: counts.downlink, c: STATUS_META.downlink.c }, { k: "prohibit", label: "ЗАПРЕТ", n: counts.prohibit, c: STATUS_META.prohibit.c }, { k: "eclipse", label: "В ТЕНИ", n: counts.eclipse, c: "#7d92c4" }, ]; return (
{tiles.map((t) => (
{t.n}
{t.label}
))}
ОПТИКА {counts.optic} ЛОКАЦИЯ {counts.radar} {counts.total} КА
); } function SatRow({ sat, status, mem, focus, onClick }) { const meta = STATUS_META[status.state]; const memColor = mem > 92 ? "#d9685f" : mem > 78 ? "#d99a4e" : "#5fb3a3"; return (
{sat.name} {status.eclipse && }{meta.t}
NORAD {sat.norad}
{Math.round(mem)}%
); } function Roster({ sats, statuses, memory, focusId, onFocus, query }) { const q = query.trim().toLowerCase(); const filtered = sats.filter((s) => !q || s.name.toLowerCase().includes(q) || String(s.norad).includes(q) || s.group.toLowerCase().includes(q)); const groups = []; let curD = null, curG = null; filtered.forEach((s) => { if (s.domain !== curD) { groups.push({ type: "domain", label: s.domain, color: s.color }); curD = s.domain; curG = null; } if (s.group !== curG) { groups.push({ type: "group", label: s.group }); curG = s.group; } groups.push({ type: "sat", sat: s }); }); return (
{groups.map((g, i) => { if (g.type === "domain") return
{g.label}
; if (g.type === "group") return
{g.label}
; const s = g.sat; return onFocus(focusId === s.id ? null : s.id)} />; })}
); } function ActivityFeed({ events, onFocus, focusId }) { return (
АКТИВНЫЕ СОБЫТИЯ {events.length}
{events.length === 0 &&
нет активных событий
} {events.map((e) => (
onFocus(e.satId)}>
{e.type}{e.target}
{e.sat}ещё {e.remain}
))}
); } function SidePanel({ sats, statuses, memory, counts, events, focusId, onFocus, query, setQuery }) { return ( ); } export { Header, SidePanel, STATUS_META, fmtDur };