// TelemetryMetricsPanel.jsx — sectioned telemetry metrics display component. // Renders microclimate, irrigation, pollination KPI cards and actuator status from a TelemetryEvent sample. // Each section is hidden when its corresponding sub-object is null / undefined, // except the actuators section which always renders and shows "—" when sample.actuators is null. // // Props: // sample {object} — TelemetryEvent-shaped object with microclimate / irrigation / // pollination / actuators sub-objects. Component returns null when sample is null. // flashKey {*} — optional value; when it changes the kpi--flash animation replays. // Pass lastSampleTs or a tick counter for automatic pulse-on-update. // className {string} — optional extra CSS class appended to the root wrapper div. // // Section visibility rules: // • microclimate section → rendered only when sample.microclimate is not null / undefined // • irrigation section → rendered only when sample.irrigation is not null / undefined // • pollination section → rendered only when sample.pollination is not null / undefined // • actuators section → always rendered; hive_gate shows "—" when sample.actuators is null // // Dependencies (expected on window): // React — globally available via Babel transform // Keeper.i18n — optional; falls back to Russian strings when absent // ActuatorStatusBar — optional; renders hive gate badge (window.ActuatorStatusBar) // ── Module-level constants (defined outside component to avoid recreation on every render) ───── // KPI anomaly thresholds matching the simulator screen thresholds. var _TMP_THRESHOLDS = { temperature_c: { warn: [32, 35], critical: [35, Infinity] }, humidity_pct: { warn: [85, 95], critical: [95, Infinity] }, co2_ppm: { warn: [900, 1200], critical: [1200, Infinity] }, substrate_moisture_pct: { warn: [0, 30], critical: [0, 15] }, substrate_ec: { warn: [3.5, 5], critical: [5, Infinity] }, drain_ec: { warn: [4, 5.5], critical: [5.5, Infinity] }, }; // Microclimate metric definitions: temperature, humidity, CO₂, insolation. var _MC_METRICS = [ { field: "temperature_c", labelKey: "sample.metrics.temperature", labelFallback: "Температура", unit: "°C", unitKey: null, icon: "ri-temp-hot-line", color: "#FF7A00", tipKey: "sample.tips.temperature", tipFallback: "Температура воздуха в теплице" }, { field: "humidity_pct", labelKey: "sample.metrics.humidity", labelFallback: "Влажность", unit: "%", unitKey: null, icon: "ri-drop-line", color: "#007BFB", tipKey: "sample.tips.humidity", tipFallback: "Относительная влажность воздуха" }, { field: "co2_ppm", labelKey: "sample.metrics.co2", labelFallback: "CO₂", unit: "ppm", unitKey: null, icon: "ri-bubble-chart-line", color: "#7FB539", tipKey: "sample.tips.co2", tipFallback: "Концентрация углекислого газа" }, { field: "insolation_lux", labelKey: "sample.metrics.insolation", labelFallback: "Освещённость", unit: "лк", unitKey: "sample.units.lux", icon: "ri-sun-line", color: "#E8B84A", tipKey: "sample.tips.insolation", tipFallback: "Уровень освещённости (люкс)" }, ]; // Irrigation metric definitions: soil moisture, drip flow, substrate EC, feed EC, drain EC. var _IRR_METRICS = [ { field: "substrate_moisture_pct", labelKey: "sample.metrics.substrateMoisture", labelFallback: "Влажность субстрата", unit: "%", unitKey: null, icon: "ri-drop-line", color: "#26972B", tipKey: "sample.tips.substrateMoisture", tipFallback: "Содержание влаги в субстрате" }, { field: "drip_flow_lh", labelKey: "sample.metrics.irrigationFlow", labelFallback: "Расход полива", unit: "л/ч", unitKey: "sample.units.litersPerHour", icon: "ri-water-flash-line", color: "#007BFB", tipKey: "sample.tips.irrigationFlow", tipFallback: "Расход капельного полива" }, { field: "substrate_ec", labelKey: "sample.metrics.substrateEc", labelFallback: "EC субстрата", unit: "mS/cm", unitKey: null, icon: "ri-flask-line", color: "#FFB300", tipKey: "sample.tips.substrateEc", tipFallback: "Электропроводность субстрата" }, { field: "feed_ec", labelKey: "sample.metrics.feedEc", labelFallback: "EC раствора", unit: "mS/cm", unitKey: null, icon: "ri-test-tube-line", color: "#7FB539", tipKey: "sample.tips.feedEc", tipFallback: "Электропроводность питательного раствора" }, { field: "drain_ec", labelKey: "sample.metrics.drainEc", labelFallback: "EC дренажа", unit: "mS/cm", unitKey: null, icon: "ri-flask-line", color: "#FF7A00", tipKey: "sample.tips.drainEc", tipFallback: "Электропроводность дренажа" }, ]; // Pollination metric definitions: departures, returns, activity index. // noSeverity: true — no threshold colouring; raw counts / index displayed as-is. var _POL_METRICS = [ { field: "departures", labelKey: "sample.metrics.departures", labelFallback: "Вылеты", unit: "", unitKey: null, icon: "ri-flight-takeoff-line", color: "#7FB539", tipKey: null, noSeverity: true }, { field: "returns", labelKey: "sample.metrics.returns", labelFallback: "Возвраты", unit: "", unitKey: null, icon: "ri-flight-land-line", color: "#26972B", tipKey: null, noSeverity: true }, { field: "activity_index", labelKey: "sample.metrics.activityIndex", labelFallback: "Индекс активности", unit: "", unitKey: null, icon: "ri-pie-chart-line", color: "#E8B84A", tipKey: null, noSeverity: true }, ]; // ── Module-level helpers ───────────────────────────────────────────────────────────────────────── // These functions only read window.Keeper.i18n globals and module-level constants. // Keeping them outside the component avoids re-creating them on every render. // i18n helper — resolves a translation key via Keeper.i18n or falls back to the provided string. function _tr(key, fallback, params) { var i18n = window.Keeper && window.Keeper.i18n ? window.Keeper.i18n : null; if (i18n && typeof i18n.t === "function") return i18n.t(key, params, { fallback: fallback }); return fallback; } // Locale resolver — returns the active locale string (defaults to "ru"). function _locale() { return (window.Keeper && window.Keeper.i18n && typeof window.Keeper.i18n.getLocale === "function") ? window.Keeper.i18n.getLocale() || "ru" : "ru"; } // Number formatter — integers: 0 decimal places; floats: 1 decimal place. function _fmt(v) { if (v == null) return "—"; if (typeof v === "number") { var decimals = Number.isInteger(v) ? 0 : 1; return new Intl.NumberFormat(_locale(), { minimumFractionDigits: decimals, maximumFractionDigits: decimals, }).format(v); } return String(v); } // Severity classifier — returns "" | "kpi--ok" | "kpi--warn" | "kpi--critical". // Returns "" when value is null / non-numeric, or when no threshold is defined for the field. // Returns "kpi--ok" when a threshold IS defined and the value falls within the normal range. function _severity(field, value) { if (value == null || typeof value !== "number") return ""; var t = _TMP_THRESHOLDS[field]; if (!t) return ""; if (t.critical && value >= t.critical[0] && value <= t.critical[1]) return "kpi--critical"; if (t.warn && value >= t.warn[0] && value <= t.warn[1]) return "kpi--warn"; return "kpi--ok"; } // Timestamp formatter — converts an ISO string to a short locale-aware HH:MM:SS string. function _fmtTs(iso) { if (!iso) return null; try { return new Intl.DateTimeFormat(_locale(), { hour: "2-digit", minute: "2-digit", second: "2-digit", }).format(new Date(iso)); } catch (_) { return iso; } } // KPI card renderer — pure given metric definitions + group data + grid class + flashKey. // flashKey is passed explicitly so this stays module-level and does not close over component props. // When flashKey changes, each card gets a new React key which re-triggers the kpi--flash animation. function _renderCards(metrics, groupData, gridCls, flashKey) { var cards = metrics.map(function(m) { var val = groupData ? groupData[m.field] : null; var sev = m.noSeverity ? "" : _severity(m.field, val); var unit = m.unitKey ? _tr(m.unitKey, m.unit) : m.unit; var label = _tr(m.labelKey, m.labelFallback); var tip = m.tipKey ? _tr(m.tipKey, m.tipFallback) : m.tipFallback; var flashCls = flashKey != null ? " kpi--flash" : ""; var cardKey = m.field + (flashKey != null ? "-" + String(flashKey) : ""); return (
{label}
{_fmt(val)} {unit ? {unit} : null}
); }); return (
{cards}
); } // ── Component ──────────────────────────────────────────────────────────────────────────────────── /** * Sectioned telemetry KPI panel rendered from a single TelemetryEvent sample. * Shows microclimate, irrigation, pollination and actuator sub-sections; each section * is hidden when its corresponding sub-object is absent in the sample. * @param {{ sample: object|null, flashKey?: *, className?: string }} props */ function TelemetryMetricsPanel({ sample, flashKey, className }) { // Stable per-instance id suffix for aria-labelledby / id pairs. // Avoids duplicate IDs when the component is mounted more than once on a page. var uid = React.useMemo(function() { return Math.random().toString(36).slice(2, 7); }, []); // ── Guard: nothing to render until a sample arrives ────────────────── if (!sample) return null; var mc = sample.microclimate != null ? sample.microclimate : null; var irr = sample.irrigation != null ? sample.irrigation : null; var pol = sample.pollination != null ? sample.pollination : null; var act = sample.actuators != null ? sample.actuators : null; var mcId = "tmp-microclimate-" + uid; var irrId = "tmp-irrigation-" + uid; var polId = "tmp-pollination-" + uid; var actId = "tmp-actuators-" + uid; var rootCls = "telemetry-metrics-panel" + (className ? " " + className : ""); return (
{/* ── Microclimate section: temperature, humidity, CO₂, insolation ── */} {mc != null && (

{_tr("sample.sections.microclimate", "Микроклимат")}

{_renderCards(_MC_METRICS, mc, "simulator-kpi-grid--four", flashKey)}
)} {/* ── Irrigation section: soil moisture, drip flow, EC values ──── */} {irr != null && (

{_tr("sample.sections.irrigation", "Полив и питание")}

{_renderCards(_IRR_METRICS, irr, "simulator-kpi-grid--three", flashKey)}
)} {/* ── Pollination section: departures, returns, activity index ──── */} {pol != null && (

{_tr("sample.sections.pollination", "Опыление")}

{_renderCards(_POL_METRICS, pol, "simulator-kpi-grid--three", flashKey)}
)} {/* ── Actuators section: hive gate state — always shown; "—" when actuators is null ── */} {(() => { var ActBar = window.ActuatorStatusBar; var gateState = act != null ? act.hive_gate : null; return (

{_tr("sample.sections.actuators", "Актуаторы")}

{_tr("sample.metrics.hiveGate", "Шлюз улья")}
{gateState != null && ActBar ? : {_tr("sample.actuators.noData", "нет данных")} }
); })()} {/* ── Freshness timestamp ── shows the time the last sample was produced ── */} {sample.timestamp && (

{_tr("sample.freshness", "Обновлено")} {" · "}

)}
); } window.TelemetryMetricsPanel = TelemetryMetricsPanel;