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 `
`;
- }
-
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 ``;
+}
+
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);
}