From da0f941d79050ba230a502b713b6d88544d44a4e Mon Sep 17 00:00:00 2001 From: Dmitry Gammel Date: Thu, 13 Aug 2026 12:42:27 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D0=B2=D1=80=D0=B0?= =?UTF-8?q?=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=D0=B5=D0=BD=D1=82=D0=B8?= =?UTF-8?q?=D0=BB=D1=8F=D1=82=D0=BE=D1=80=D0=B0=20+=20=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D0=B8=D0=B3?= =?UTF-8?q?=D0=BD=D0=B0=D0=BB=D0=BE=D0=B2=20=D0=BD=D0=B0=20=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D0=BE=D0=B3=D0=BE=D0=B2=D1=8B=D0=B5/=D0=B4=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D1=80=D0=B5=D1=82=D0=BD=D1=8B=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit transform-origin: center у SVG-группы лопастей считался от bounding box, а не от геометрического центра ступицы (0,0) — вентилятор вращался вокруг смещённой точки. Зафиксировано в 0 0. Датчик больше не подписан как «DHT22» (сбивало с толку при добавлении дискретных сигналов вроде door_open) — просто «Датчик», а сами сигналы в карточке визуально разбиты на группы «Аналоговые»/«Дискретные». --- internal/httpserver/web/index.html | 59 ++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/internal/httpserver/web/index.html b/internal/httpserver/web/index.html index c7e8a81..eca9387 100644 --- a/internal/httpserver/web/index.html +++ b/internal/httpserver/web/index.html @@ -47,7 +47,12 @@ .peripheral-label { font-size: 12px; fill: var(--text); font-weight: 600; text-anchor: middle; } .peripheral-sub { font-size: 9px; fill: var(--muted); text-anchor: middle; } - .fan-blades { transform-origin: center; transition: opacity .2s; } + /* The blade ellipses are laid out so their geometric centroid is the + group's local (0,0) — but CSS "transform-origin: center" for SVG uses + the bounding box center, which isn't the same point (the ellipses' + 144x28-ish extents aren't symmetric around 0,0), so it wobbled around + an off-center point instead of spinning cleanly. Pin it to 0 0. */ + .fan-blades { transform-origin: 0px 0px; transition: opacity .2s; } .fan-blades.on { animation: spin 0.9s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @@ -107,6 +112,12 @@ .value { color: var(--text); font-weight: 600; } .hint { color: var(--muted); font-size: 0.78rem; margin-top: 10px; } + .signal-group-label { + font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em; + color: var(--muted); margin-top: 12px; margin-bottom: 2px; + } + .signal-group-label:first-child { margin-top: 0; } + .signal-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 0; border-top: 1px solid var(--border); font-size: 0.85rem; @@ -184,7 +195,7 @@ 24°C 55% - Датчик DHT22 + Датчик GPIO4 · input @@ -289,17 +300,12 @@ function cardHTML(slot) { document.getElementById('cards').innerHTML = Object.keys(SLOTS).map(cardHTML).join(''); -function signalRowHTML(slot, name, value) { +function isDiscreteSignal(slot, name) { + return knownSignals[slot]?.discrete?.has(name); +} + +function analogRowHTML(slot, name, value) { const preset = SIGNAL_PRESETS[name]; - const isDiscrete = !preset && (value === 0 || value === 1) && knownSignals[slot]?.discrete?.has(name); - - if (isDiscrete) { - return `
- ${name} -
-
`; - } - const min = preset?.min ?? 0, max = preset?.max ?? 100, step = preset?.step ?? 1, unit = preset?.unit ?? ''; return `
${name}${unit ? ' ('+unit+')' : ''} @@ -309,14 +315,37 @@ function signalRowHTML(slot, name, value) {
`; } +function discreteRowHTML(slot, name, value) { + return `
+ ${name} +
+
`; +} + function cssId(name) { return name.replace(/[^a-zA-Z0-9_-]/g, '_'); } +// Discrete vs analog are shown as two visually separate groups — a +// discrete "door_open" signal is still just another sensor_type on the +// same physical device underneath (one MQTT device can report several +// sensor_types, same as real telemetry), but grouping keeps it from +// reading as "attached to the temperature sensor". function renderSignals(slot, readings) { const container = document.getElementById(`${slot}-signals`); - const names = Object.keys(readings); - // Preserve discrete-ness across re-renders (server only stores numbers). knownSignals[slot] = knownSignals[slot] || { discrete: new Set() }; - container.innerHTML = names.map((name) => signalRowHTML(slot, name, readings[name])).join(''); + + const analogNames = orderedNames(readings).filter((n) => !isDiscreteSignal(slot, n)); + const discreteNames = Object.keys(readings).filter((n) => isDiscreteSignal(slot, n)).sort(); + + let html = ''; + if (analogNames.length) { + html += `
Аналоговые
`; + html += analogNames.map((name) => analogRowHTML(slot, name, readings[name])).join(''); + } + if (discreteNames.length) { + html += `
Дискретные
`; + html += discreteNames.map((name) => discreteRowHTML(slot, name, readings[name])).join(''); + } + container.innerHTML = html; updateBoardText(slot, readings); }