frugal-iot-client 1.3.13 → 1.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +12 -0
- package/CARDS_PLAN.md +841 -0
- package/CARDS_UX.md +1022 -0
- package/CLAUDE.md +128 -14
- package/FLASH_PLAN.md +250 -9
- package/admin.js +1377 -0
- package/base/boards.json +6 -4
- package/base/bootloader/esp32c3.bin +0 -0
- package/base/bootloader/esp32s2.bin +0 -0
- package/base/bootloader/esp32s3.bin +0 -0
- package/cards.js +867 -0
- package/core.js +2696 -0
- package/dashboard.js +12 -0
- package/dashboard_example.html +2 -2
- package/flash.js +609 -0
- package/frugaliot.css +342 -14
- package/graph.js +475 -0
- package/index-old.html +37 -0
- package/index.html +6 -6
- package/nodeview.js +433 -0
- package/package.json +11 -4
- package/test/cards.test.js +227 -0
- package/test/cardui.test.js +435 -0
- package/test/datatree.test.js +171 -0
- package/test/fixtures/config.json +1416 -0
- package/test/fixtures/regenerate.js +40 -0
- package/test/grid.test.js +258 -0
- package/test/lightdom.test.js +57 -0
- package/test/mock.html +113 -0
- package/test/mock.js +269 -0
- package/test/modules.test.js +48 -0
- package/test/oldui.test.js +40 -0
- package/test/page.test.js +292 -0
- package/test/permissions.test.js +134 -0
- package/test/serialize.js +49 -0
- package/test/setup.js +54 -0
- package/test/smoke.test.js +43 -0
- package/test/snapshots/card-control-wired-back.txt +197 -0
- package/test/snapshots/card-control-wired-front.txt +60 -0
- package/test/snapshots/card-mixed-sensors-summary.txt +14 -0
- package/test/snapshots/card-one-device-summary.txt +14 -0
- package/test/snapshots/control-wired.txt +264 -0
- package/test/snapshots/default-front.txt +225 -0
- package/test/snapshots/mixed-sensors.txt +208 -0
- package/test/snapshots/no-readings.txt +54 -0
- package/test/snapshots/one-device.txt +154 -0
- package/test/snapshots/unknown-module.txt +117 -0
- package/webcomponents.js +15 -5541
- package/widgets.js +760 -0
package/CARDS_UX.md
ADDED
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
# Device Cards — UX Design
|
|
2
|
+
|
|
3
|
+
Step 1 of 3: **UX design** → implementation plan (`CARDS_PLAN.md`) → implementation.
|
|
4
|
+
|
|
5
|
+
This document is the thing to argue with. Every place marked **D-n** is a decision that is open;
|
|
6
|
+
each carries a recommendation so it can be accepted by deleting the alternatives. Implementation
|
|
7
|
+
notes accumulate in the appendix and move to `CARDS_PLAN.md` in step 2.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. What this replaces
|
|
12
|
+
|
|
13
|
+
Today a project renders as a vertical stack of `mqtt-node` elements, each containing one
|
|
14
|
+
`<details>` drop-down per module (`MqttGroup`), each drop-down showing a summary line when closed
|
|
15
|
+
and its topic widgets when open. Nesting is two levels deep (`insidefrugaliot` modules hide inside
|
|
16
|
+
the `frugal_iot` drop-down). To read one device's temperature you open the device, find the SHT
|
|
17
|
+
drop-down, open it.
|
|
18
|
+
|
|
19
|
+
The new model: **one device = one card, with three display modes.** Drop-downs disappear from the
|
|
20
|
+
main path.
|
|
21
|
+
|
|
22
|
+
## 2. Vocabulary
|
|
23
|
+
|
|
24
|
+
| UI word | Code word | Note |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| Device | node / `MqttNode` / `MqttTopicNode` | UI text says "device"; code keeps `node` |
|
|
27
|
+
| Card | (new) `mqtt-devicecard` | one per device |
|
|
28
|
+
| Reading | topic with `rw: r` | a measurement, e.g. `sht/temperature` |
|
|
29
|
+
| Actuator | topic with `rw: w` | drives hardware directly — `relay/on`, `ledbuiltin/on`, `brightness` |
|
|
30
|
+
| Control | a control module | a loop with both inputs and outputs, e.g. `controlhysteresis`, `controlblinken` |
|
|
31
|
+
| Module | group / `MqttGroup` | e.g. `sht`, `relay`, `controlhysteresis` |
|
|
32
|
+
|
|
33
|
+
"Actuator" and "control" are deliberately separate. An actuator is one writable topic that changes
|
|
34
|
+
the world when you set it. A control is a whole module that reads inputs, applies a rule, and drives
|
|
35
|
+
an output — so it is never a single topic, and its inputs (`limit`, `greater`, `hysteresis`) are
|
|
36
|
+
settings, not actuators.
|
|
37
|
+
|
|
38
|
+
> **Detecting a control module.** The code tests `groupId.startsWith('control')`
|
|
39
|
+
> (`MqttTopic.controlGroups`, and the `frugaliot:controlgroup` dispatch in
|
|
40
|
+
> `MqttTopicNode.addGroupFromTemplate`). That test used to miss `climate` — a control module by every
|
|
41
|
+
> structural measure that did not start with "control". **`climate` has since been deleted** as
|
|
42
|
+
> legacy (see D-16), so the prefix test is correct again for every module that exists today.
|
|
43
|
+
>
|
|
44
|
+
> It is still correct only by luck: the next control module named `thermostat` or `irrigation`
|
|
45
|
+
> breaks it silently. A module-level `control: true` in `modules.yaml` is three lines of YAML and
|
|
46
|
+
> removes the naming dependency — now a robustness fix rather than a bug fix. **D-16**
|
|
47
|
+
|
|
48
|
+
## 3. The three modes
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
┌──────────┐ click card ┌──────────┐ click ⚙ / gear ┌──────────┐
|
|
52
|
+
│ SUMMARY │ ────────────► │ FRONT │ ────────────────► │ BACK │
|
|
53
|
+
│ 1 line │ ◄──────────── │ readings │ ◄──────────────── │ settings │
|
|
54
|
+
└──────────┘ click ⌄ └──────────┘ click ✕ / done └──────────┘
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
One `<mqtt-devicecard mode="summary|front|back">`. Mode is per-device, remembered (§7).
|
|
58
|
+
Several cards may be on FRONT at once — users compare devices, so this is not an accordion. **D-1**
|
|
59
|
+
|
|
60
|
+
### 3.1 SUMMARY — "is it alive and roughly what is it saying"
|
|
61
|
+
|
|
62
|
+
Full-width row on mobile; a responsive grid cell on desktop. The whole card is one click target.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
|
|
66
|
+
│ ● Greenhouse North 78% │ │ ● Pond Pump 91% │
|
|
67
|
+
│ 30.1 °C 85 %RH │ │ Pump ✓ Soil 38 % │
|
|
68
|
+
└─────────────────────────────────┘ └─────────────────────────────────┘
|
|
69
|
+
┌─────────────────────────────────┐
|
|
70
|
+
│ ◌ Shed Sensor 2h ago │ ← stale: values dimmed, age instead of battery
|
|
71
|
+
│ 29.4 °C 81 %RH │
|
|
72
|
+
└─────────────────────────────────┘
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- Line 1: status dot, device name (`frugal_iot/name`, falling back to the device id), battery % or,
|
|
76
|
+
when stale, "2h ago".
|
|
77
|
+
- Line 2: the summary chips (§4.1).
|
|
78
|
+
- Nothing on SUMMARY is interactive — the card is a single tap target. A relay toggle sitting in a
|
|
79
|
+
summary invites mis-taps; controls are reachable one tap away. **D-2**
|
|
80
|
+
|
|
81
|
+
### 3.2 FRONT — "what is it doing, in detail"
|
|
82
|
+
|
|
83
|
+
**No module structure on the front.** No module headers, no grouping, no nesting — a flat ordered
|
|
84
|
+
list of rows. Which sensor a reading came from is a fact about the hardware, not something the
|
|
85
|
+
person watching the greenhouse needs. Module structure returns on the back, where it is the right
|
|
86
|
+
organising principle. **D-17**
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
┌───────────────────────────────────────────────┐
|
|
90
|
+
│ ● Greenhouse North ⌄ ⚙ │ ⌄ collapse ⚙ flip to back
|
|
91
|
+
├───────────────────────────────────────────────┤
|
|
92
|
+
│ Temperature 30.1 °C 📈│
|
|
93
|
+
│ ├────────────────────────█──────────────────┤ │
|
|
94
|
+
│ 0 50 │
|
|
95
|
+
│ Humidity 85 %RH 📈│
|
|
96
|
+
│ ├───────────────────────────────█───────────┤ │
|
|
97
|
+
│ 0 100 │
|
|
98
|
+
│ Heating ●──○ ON 📈 │
|
|
99
|
+
│ Control Temperature > 32 ±3 ✓ ON │
|
|
100
|
+
│ Vent light ○──● OFF 📈 │
|
|
101
|
+
├───────────────────────────────────────────────┤
|
|
102
|
+
│ 🔋 3.94 V · seen 12s ago │
|
|
103
|
+
└───────────────────────────────────────────────┘
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Exactly three kinds of row, in whatever order the device declares (§4.2):
|
|
107
|
+
|
|
108
|
+
1. **Reading** — label, value + unit, and the widget its `display:` calls for (bar here), with
|
|
109
|
+
min/max end labels and the graph icon `mqtt-bar` already has.
|
|
110
|
+
2. **Actuator** — label and a live toggle or slider. **Tappable on the front**: turning a pump on is
|
|
111
|
+
the commonest thing anyone does in the field, and making them flip the card to do it is a real
|
|
112
|
+
cost. The mis-tap worry of D-2 was about the *summary*, where the whole card is one target; here
|
|
113
|
+
the card is already open and the toggle is a deliberate, properly sized target. **D-3**
|
|
114
|
+
3. **Control** — read-only. Shows the rule and the resulting output state
|
|
115
|
+
(`Temperature > 32 ±3 ✓ ON`). Editing the rule happens on the back. So the split is *act on
|
|
116
|
+
the front, configure on the back*, which keeps the symmetry rather than breaking it.
|
|
117
|
+
|
|
118
|
+
**Actuators and controls stay separate rows**, even when a control drives that actuator. Merging
|
|
119
|
+
them into one row was considered and dropped: it hides the fact that two distinct things exist, and
|
|
120
|
+
the interesting question — what happens when you tap an actuator a control is driving — has its own
|
|
121
|
+
UX that is deliberately **deferred** (§14). Until that lands, tapping a control-driven actuator will
|
|
122
|
+
be overwritten by the loop on its next cycle. That is a known rough edge, recorded rather than
|
|
123
|
+
designed around. **D-18**
|
|
124
|
+
|
|
125
|
+
Footer strip: battery, last seen. Small, gray, always present.
|
|
126
|
+
|
|
127
|
+
### 3.3 BACK — "settings and background"
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
┌───────────────────────────────────────────────┐
|
|
131
|
+
│ ⚙ Greenhouse North ✕ │
|
|
132
|
+
├───────────────────────────────────────────────┤
|
|
133
|
+
│ DEVICE │
|
|
134
|
+
│ Name [Greenhouse North ] │
|
|
135
|
+
│ Description [Bed 3, north end ] │
|
|
136
|
+
│ Device id esp8266-fb94bb │
|
|
137
|
+
│ Last seen 12s ago (14:32:05) │
|
|
138
|
+
│ Battery 3.94 V Wifi ▂▄▆ "shed-ap" │
|
|
139
|
+
│ OTA key a1b2c3d4 ⧉ │
|
|
140
|
+
├───────────────────────────────────────────────┤
|
|
141
|
+
│ CONTROL — Fan │
|
|
142
|
+
│ Input [Temperature (SHT) ▾] 30.1 │
|
|
143
|
+
│ When [>] [ 32.0 ] ± [ 3.0 ] │
|
|
144
|
+
│ Output [Relay 1 ▾] ✓ ON │
|
|
145
|
+
│ Manual override [ ] │
|
|
146
|
+
├───────────────────────────────────────────────┤
|
|
147
|
+
│ SHT │
|
|
148
|
+
│ Temperature 30.1 °C min 0 max 50 📈 │
|
|
149
|
+
│ Humidity 85 %RH min 0 max 100 📈 │
|
|
150
|
+
├───────────────────────────────────────────────┤
|
|
151
|
+
│ RELAY │
|
|
152
|
+
│ Relay 1 [x] on 📈 │
|
|
153
|
+
├───────────────────────────────────────────────┤
|
|
154
|
+
│ ADVANCED ⌄ │
|
|
155
|
+
└───────────────────────────────────────────────┘
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- Flat sections in a fixed order — **no drop-downs**, agreeing with the instinct in the brief that
|
|
159
|
+
someone on the back of a card wants everything about that sensor. Order: Device → Controls →
|
|
160
|
+
Readings by module → Advanced.
|
|
161
|
+
- ADVANCED is the one thing still collapsed: raw MQTT topic paths, wiring internals, flash/OTA
|
|
162
|
+
actions. It is for debugging, not for the field. **D-4**
|
|
163
|
+
- Editable min/max per reading is shown above as a stretch (`min 0 max 50`); the schema already
|
|
164
|
+
carries `min`/`max` per topic, and per-device overrides arrive on `…/set/…/max`. **D-5**
|
|
165
|
+
- **The control line follows `dashboard_example.html`, not the old node UI.** One inline row,
|
|
166
|
+
`[>] [ 32.0 ] ± [ 3.0 ]`: the comparison is a two-state control showing `<` or `>`, the limit and
|
|
167
|
+
the hysteresis are bare number fields, and the hysteresis is labelled with the `±` symbol rather
|
|
168
|
+
than the word "Hysteresis". The dashboard already builds precisely this — an `mqtt-toggle` with
|
|
169
|
+
`labels: '<,>'` plus two `mqtt-text` fields with `label: ''` and `label: '±'`. A two-state toggle
|
|
170
|
+
is one tap where a dropdown is two, so prefer the toggle over a `▾` menu. **D-19**
|
|
171
|
+
|
|
172
|
+
### 3.4 How the flip behaves
|
|
173
|
+
|
|
174
|
+
- **Desktop**: a real CSS `rotateY` flip. This requires front and back to occupy the same box, so
|
|
175
|
+
the back scrolls internally at the front's height rather than growing the card. Same width, same
|
|
176
|
+
height, no layout jump for neighbouring cards.
|
|
177
|
+
- **Mobile**: no flip. The back opens as a full-screen sheet sliding up from the bottom, with the
|
|
178
|
+
device name and a ✕ in its header. A 3D flip in a 340px-wide column, with form fields on the
|
|
179
|
+
reverse, is worse than the sheet that every mobile OS has trained people on. **D-6**
|
|
180
|
+
- The metaphor survives either way: front = watch, back = adjust.
|
|
181
|
+
|
|
182
|
+
## 4. How modules accumulate into a card
|
|
183
|
+
|
|
184
|
+
This is the core question in the brief. Three levels of declaration, each answering one question:
|
|
185
|
+
|
|
186
|
+
| Level | File | Question it answers |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| Topic | `topics.yaml` | how is one reading rendered (`display`, `units`, `width`, `color`, `min`, `max`) |
|
|
189
|
+
| Module | `modules.yaml` | which of *my* topics belong in *my* summary |
|
|
190
|
+
| Device | org/project config | what appears on the summary and the front of *this* device, in what order |
|
|
191
|
+
|
|
192
|
+
Note what is **not** here: nothing declares a render type twice. `display:` already carries
|
|
193
|
+
`bar|gauge|text|toggle|slider|color` per topic, so the summary and front lists say only *which*
|
|
194
|
+
topics and *in what order* — never *how*.
|
|
195
|
+
|
|
196
|
+
### 4.1 Summary content — a list of twigs, not a format string
|
|
197
|
+
|
|
198
|
+
The brief suggests a format string:
|
|
199
|
+
`'${sht/temperature} ${sht/temperature/unit} ${frugaliot/battery}${frugaliot/battery/unit}'`
|
|
200
|
+
|
|
201
|
+
**Recommendation: an ordered list of twigs instead.** **D-7**
|
|
202
|
+
|
|
203
|
+
```yaml
|
|
204
|
+
# in a device / project config
|
|
205
|
+
summary: [sht/temperature, sht/humidity, battery/battery]
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Why not a format string:
|
|
209
|
+
- Units, names and number formatting all need translating and localising; baking them into a
|
|
210
|
+
string in YAML puts untranslatable text in the config.
|
|
211
|
+
- A missing value (sensor absent, device never seen) has to render as something sensible — a
|
|
212
|
+
format string gives `undefined °C`.
|
|
213
|
+
- Values want individual styling: schema colour, dimming when stale, red when out of range.
|
|
214
|
+
- A list is a strict subset of what a format string can express, minus arbitrary literal text —
|
|
215
|
+
which nothing in the summary actually needs.
|
|
216
|
+
|
|
217
|
+
**Default when nothing is declared**: use each module's existing `summaryText()`. The machinery is
|
|
218
|
+
already built — `MqttGroupSht` returns `30.1°C 85%RH`, `MqttGroupRelay` returns `✓`,
|
|
219
|
+
`MqttGroupControlHysteresis` returns the whole control line.
|
|
220
|
+
|
|
221
|
+
**A module opts *out*, not in** — `summary: false`. **D-37** An earlier draft had `summary: true` as
|
|
222
|
+
the opt-in; writing it revealed that 23 of the 33 modules would carry the flag, which is a flag doing
|
|
223
|
+
no work. Inverted, five modules carry it: `button` (transient), `captive`, `carousel` and `lcd` (not
|
|
224
|
+
readings), and `frugal_iot` (the card identity, not a module on it). `insidefrugaliot` modules are excluded structurally and need no flag. (`gps` was in that list on
|
|
225
|
+
first pass and taken out — a position does belong on a summary.)
|
|
226
|
+
|
|
227
|
+
The polarity also fails in the right direction: a newly added sensor module shows up in summaries by
|
|
228
|
+
default, rather than being invisible until someone remembers a flag.
|
|
229
|
+
|
|
230
|
+
**Done, and without the YAML.** L-3 set out to replace the hand-written summaries with a
|
|
231
|
+
`summary: [leaves]` list per module. What it actually needed was a sensible default:
|
|
232
|
+
`MqttTopicGroup.summaryText()` now returns the module's own readings, formatted by the schema and
|
|
233
|
+
capped at two. That is what §4.1 always said the fallback should be, and it gives **26 of the 33
|
|
234
|
+
modules** a summary rather than the ten that had one written by hand — a device with no `devices.yaml`
|
|
235
|
+
entry used to show a blank line if its modules happened to be among the other twenty.
|
|
236
|
+
|
|
237
|
+
Four of the hand-written subclasses were deleted because the generic version produces exactly what
|
|
238
|
+
they did. Three remain, and each earns it: `relay` wants its name beside the tick, `ota` shows a key
|
|
239
|
+
rather than a reading, and a control shows a rule rather than a list of values. No `modules.yaml`
|
|
240
|
+
change was needed at all. **D-44**
|
|
241
|
+
|
|
242
|
+
The paragraphs below describe the interim state and are kept for the reasoning:
|
|
243
|
+
|
|
244
|
+
**This is explicitly an interim step.** Only a handful of modules have a hand-coded `summaryText()`
|
|
245
|
+
(`sht`, `dht`, `ht`, `soil`, `ds18b20`, `battery`, `relay`, `ledbuiltin`, `ota`,
|
|
246
|
+
`controlhysteresis`) — every other module has no summary at all, and each one that does required a
|
|
247
|
+
subclass. The intent is to delete those subclasses. The migration needs no new mechanism, because
|
|
248
|
+
the *same twig list* works one level up:
|
|
249
|
+
|
|
250
|
+
```yaml
|
|
251
|
+
sht:
|
|
252
|
+
name: SHT
|
|
253
|
+
summary: [temperature, humidity] # leaves, not twigs — replaces MqttGroupSht.summaryText()
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
So there is one mechanism at both levels: a module declares its own default summary as a list of
|
|
257
|
+
leaves, a device may override with a list of twigs. As each module gains a `summary:` list, its
|
|
258
|
+
hand-coded class goes. **D-20**
|
|
259
|
+
|
|
260
|
+
**Controls are the exception and stay hand-coded.** `MqttGroupControlHysteresis.summaryText()`
|
|
261
|
+
produces `Fan = Temperature > 32 ±3 ✓` — a structural sentence with wiring lookups and a comparison
|
|
262
|
+
operator in the middle, not a list of values. A list cannot express it and a format string would
|
|
263
|
+
express it badly. Control modules keep a purpose-built renderer; only *sensor* summaries become
|
|
264
|
+
declarative.
|
|
265
|
+
|
|
266
|
+
**Fallback order** for a module with no `summary:` list and no `summaryText()`: its `graphable`
|
|
267
|
+
readings, capped at two, else nothing. A module contributing nothing is not an error — a device
|
|
268
|
+
whose summary line is empty still shows its name and status, which is the important part.
|
|
269
|
+
|
|
270
|
+
### 4.2 Front content — a per-device ordered list
|
|
271
|
+
|
|
272
|
+
An earlier draft put `front: bar|value|none` on each topic in `modules.yaml`. That was wrong: it
|
|
273
|
+
duplicates `display:`, which already says `bar|gauge|text|toggle|slider|color`. Two keys meaning
|
|
274
|
+
"how do I render" is one key too many, and they would eventually disagree.
|
|
275
|
+
|
|
276
|
+
What is actually needed is *which* topics appear and *in what order* — and that is a per-device
|
|
277
|
+
question, because a temperature logger wants the temperature at the top with its control beneath,
|
|
278
|
+
while another device wants something else entirely. Same shape as the summary list: **D-21**
|
|
279
|
+
|
|
280
|
+
```yaml
|
|
281
|
+
# device level
|
|
282
|
+
front:
|
|
283
|
+
- sht/temperature
|
|
284
|
+
- controlhysteresis # a bare module id = that control's row
|
|
285
|
+
- sht/humidity
|
|
286
|
+
- relay/on
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
An entry is either a **twig** (a reading or an actuator, rendered per its `display:`) or a **module
|
|
290
|
+
id** (a control module, rendered as its one control row). Order is the list order, so readings,
|
|
291
|
+
actuators and controls interleave freely — no fixed section order to fight.
|
|
292
|
+
|
|
293
|
+
**Default when no `front:` is declared**, so a newly discovered device is never blank:
|
|
294
|
+
|
|
295
|
+
1. every reading with `graphable: true`, in module declaration order, then
|
|
296
|
+
2. every actuator (`rw: w`, not inside a control module), then
|
|
297
|
+
3. one row per control module.
|
|
298
|
+
|
|
299
|
+
Excluded from (1) and (2): modules marked `insidefrugaliot` — `battery`, `health` — which feed the
|
|
300
|
+
status strip instead (§4.4), and topics belonging to a control module, which the control's own row
|
|
301
|
+
already represents. `graphable: true` turns out to be a good proxy for "a real measurement rather
|
|
302
|
+
than a diagnostic field": it is set on 33 of the 54 topics in `topics.yaml`, and the ones it leaves
|
|
303
|
+
out are exactly the writable settings and the text/metadata fields. **D-22**
|
|
304
|
+
|
|
305
|
+
If that default proves wrong in practice, the fix is a module-level boolean, not a second render
|
|
306
|
+
key.
|
|
307
|
+
|
|
308
|
+
### 4.3 Labels
|
|
309
|
+
|
|
310
|
+
On the **front**, a row is labelled by the topic's `name` from `topics.yaml` — `Temperature`,
|
|
311
|
+
`Humidity` — with no module name attached, per §3.2.
|
|
312
|
+
|
|
313
|
+
Where two front rows would carry the same label, the collision is resolved by using the **module's**
|
|
314
|
+
`name` instead of the topic's: a device with both an SHT and a DS18B20 shows `Temperature` and
|
|
315
|
+
`Soil Temperature`, because `ds18b20` is already named "Soil Temperature" in `modules.yaml`. This is
|
|
316
|
+
better than `Temperature (SHT)` / `Temperature (DS18B20)` — it reads as a description of what is
|
|
317
|
+
being measured rather than as an exposed part number. A device-level `label:` on a `front:` entry
|
|
318
|
+
overrides both. **D-8**
|
|
319
|
+
|
|
320
|
+
On the **back**, readings sit under a module heading, so the label is always just the topic `name` —
|
|
321
|
+
the heading carries the module identity.
|
|
322
|
+
|
|
323
|
+
### 4.4 `insidefrugaliot` modules
|
|
324
|
+
|
|
325
|
+
`battery`, `health` and friends already nest inside the `frugal_iot` group. They do not become
|
|
326
|
+
sections; they feed the status strip (battery, wifi, last seen) on SUMMARY/FRONT and the DEVICE
|
|
327
|
+
section on BACK. `frugal_iot` itself is never a section — it *is* the card's identity.
|
|
328
|
+
|
|
329
|
+
### 4.5 Units are not usable as they stand
|
|
330
|
+
|
|
331
|
+
The brief's example summary, "SHT 30° 85%", assumes a unit after the number. Two problems:
|
|
332
|
+
|
|
333
|
+
- **The client never reads `units`.** The key exists in `topics.yaml` on 18 topics; `webcomponents.js`
|
|
334
|
+
contains no reference to it at all. Today's UI shows bare numbers, and the `°C`/`%RH` in
|
|
335
|
+
`MqttGroupSht.summaryText()` is a hard-coded string in the class.
|
|
336
|
+
- **The values are SenML codes, not display symbols.** `units: Cel`, `deg`, `lx`, `lat`, `lon`,
|
|
337
|
+
`kOhm`, `mg/L`. Rendered literally that reads "30.1 Cel".
|
|
338
|
+
|
|
339
|
+
So the card UI needs a small SenML-code → display-symbol map (`Cel` → `°C`, `deg` → `°`, `%RH` and
|
|
340
|
+
`mV` pass through unchanged), and `units:` needs filling in on the topics that lack it. Neither is
|
|
341
|
+
large, but neither is free, and the summary line does not work without both.
|
|
342
|
+
|
|
343
|
+
**Recommendation: a separate lookup table, not the `languages` table.** **D-23** Three reasons, the
|
|
344
|
+
first decisive:
|
|
345
|
+
|
|
346
|
+
1. **`el()`'s translation filters actively reject unit symbols.** Look at the two guards at
|
|
347
|
+
`webcomponents.js:1092-1094`: a value containing `/` is skipped, and a value whose first
|
|
348
|
+
character is not an ASCII letter is skipped. That excludes `°C`, `%RH`, `m/s`, `mm/hr` and
|
|
349
|
+
`mg/L` outright. Units would have to special-case their way around the very mechanism they were
|
|
350
|
+
put into.
|
|
351
|
+
2. **`getString` fails unreadably for codes.** It falls back to the tag itself, so a missing entry
|
|
352
|
+
renders the literal `Cel`. A missing *English string* at least still reads as English; a missing
|
|
353
|
+
*code* renders as noise. Keying a lookup table by code is fine; keying a translation table by
|
|
354
|
+
code conflates "identifier → display" with "English → other language".
|
|
355
|
+
3. **It keeps unit *conversion* separate from unit *translation*.** °C → °F is a user preference
|
|
356
|
+
that has nothing to do with language — a French speaker wants °C, an American English speaker
|
|
357
|
+
may want °F. Folding units into `languages` invites conflating the two and closes that door.
|
|
358
|
+
|
|
359
|
+
The escape hatch survives either way: if a unit ever genuinely needs translating (`%RH` → `%HR` in
|
|
360
|
+
French), the table's *value* can be a `getString()` tag. Starting separate does not prevent that;
|
|
361
|
+
starting inside `languages` does force four-language duplication for all ~18 entries today.
|
|
362
|
+
|
|
363
|
+
### 4.6 Which `devices.yaml` entry applies
|
|
364
|
+
|
|
365
|
+
The OTA key is built by the firmware as `SYSTEM_OTA_PREFIX "_" SYSTEM_OTA_SUFFIX`
|
|
366
|
+
(`src/system/ota.cpp:220`), where the prefix is the application and the suffix is the board. One app
|
|
367
|
+
across ten boards therefore produces ten keys — `sht30_c3_pico`, `sht30_d1_mini`,
|
|
368
|
+
`sht30_s2_mini_4x`, … — all wanting the same card layout, because the layout follows the
|
|
369
|
+
*application*, not the board.
|
|
370
|
+
|
|
371
|
+
So `devices.yaml` keys **match by prefix**, resolved in this order: **D-34**
|
|
372
|
+
|
|
373
|
+
1. an exact match on the **device id** (`esp8266-fb94bb`) — one device overriding its build;
|
|
374
|
+
2. an exact match on the **full OTA key** (`sht30_c3_pico`) — one board differing from its siblings;
|
|
375
|
+
3. the **longest key** `k` for which `otakey.startsWith(k)` (`sht30`) — the normal case.
|
|
376
|
+
|
|
377
|
+
Longest-match, not file order, so adding a more specific entry never depends on where it is placed
|
|
378
|
+
and `sht30` correctly beats `sht` when both exist.
|
|
379
|
+
|
|
380
|
+
Prefix matching is not merely convenient here, it is necessary: suffixes themselves contain
|
|
381
|
+
underscores (`c3_pico`, `s2_mini_4x`), so `key.split('_')[0]` does not recover the application name.
|
|
382
|
+
`startsWith` does.
|
|
383
|
+
|
|
384
|
+
The practical effect is that `devices.yaml` has roughly one entry per *example application*, not per
|
|
385
|
+
device or per build.
|
|
386
|
+
|
|
387
|
+
### 4.7 Precedence for the summary line
|
|
388
|
+
|
|
389
|
+
Three sources, first match wins: **D-36**
|
|
390
|
+
|
|
391
|
+
1. the device entry's `summary:` list;
|
|
392
|
+
2. otherwise the **first two entries of its `front:` list** — so the common device declares `front:`
|
|
393
|
+
only and gets a sensible summary for free;
|
|
394
|
+
3. otherwise every contributing module (§4.1).
|
|
395
|
+
|
|
396
|
+
Rules (2) and (3) are capped at **four** chips, which is enough for temperature + humidity + air
|
|
397
|
+
quality + a control — two was too few, and an ENS160 device could not say what it was for. A declared
|
|
398
|
+
`summary:` list is not capped: if an author asks for six, they get six. **D-36**
|
|
399
|
+
|
|
400
|
+
A **control contributes a chip, not its rule**: `Relay ✓`, where the front row shows
|
|
401
|
+
`Relay = Temperature > 32 ±3 ✓`. Two methods, `summaryShort()` and `summaryText()`, because a
|
|
402
|
+
sentence in a chip row wraps the card to three lines. **D-38**
|
|
403
|
+
|
|
404
|
+
A **control that drives nothing is left off the summary altogether** — a device often carries a
|
|
405
|
+
control that was never wired up, and an idle one should not take a place on a four-chip line. It
|
|
406
|
+
still appears on the front, which is where you would go to wire it. **D-40**
|
|
407
|
+
|
|
408
|
+
`summary:` is kept as a separate key rather than always derived, because the one-line view genuinely
|
|
409
|
+
differs from the full one often enough to matter — a soil sensor whose summary is the moisture but
|
|
410
|
+
whose front leads with the control, or a relay whose summary is just its state while its front leads
|
|
411
|
+
with the sensor driving it. Rule (2) means most devices never write it.
|
|
412
|
+
|
|
413
|
+
Entries in both lists are a **twig** (contains `/`) or a **module id** (does not), as §4.2.
|
|
414
|
+
|
|
415
|
+
## 5. Status, staleness and number formatting
|
|
416
|
+
|
|
417
|
+
Three things the brief does not cover but which decide whether the UI is usable in the field.
|
|
418
|
+
|
|
419
|
+
**Status.** A frugal device's most important property is whether it is still reporting. One dot
|
|
420
|
+
with three states, shape-differentiated as well as coloured (sunlight, colour blindness):
|
|
421
|
+
|
|
422
|
+
| State | Dot | Meaning |
|
|
423
|
+
|---|---|---|
|
|
424
|
+
| Live | ● green | seen within the watchdog interval |
|
|
425
|
+
| Stale | ◌ amber | seen, but not recently |
|
|
426
|
+
| Offline | ○ gray | watchdog expired / never seen |
|
|
427
|
+
|
|
428
|
+
**Stale values stay visible, dimmed, with their age.** A two-hour-old reading is still
|
|
429
|
+
information; hiding it is a regression from today's behaviour, which greys the whole node.
|
|
430
|
+
|
|
431
|
+
**Number formatting — `width`.** `30.142857 °C` is unusable and is what MQTT currently delivers.
|
|
432
|
+
The node/firmware already has the concept of **width**, so use the same word rather than inventing
|
|
433
|
+
`precision`: `width` is the total character count of the rendered number, **including the sign and
|
|
434
|
+
the decimal point, excluding units**. Add it to `topics.yaml`, overridable per module in
|
|
435
|
+
`modules.yaml` like any other topic field. **D-9**
|
|
436
|
+
|
|
437
|
+
**Only `float` and `exponential` topics need it** — 25 of the 54 topics in `topics.yaml`, not all of
|
|
438
|
+
them. **D-35** An integer has no decimals to decide, so `width` would carry no information a
|
|
439
|
+
renderer cannot already derive: the field width for right-alignment is
|
|
440
|
+
`max(len(formatInt(min)), len(formatInt(max)))`, and an int with no declared range is simply rendered
|
|
441
|
+
as it arrives. `bool`, `text` and `color` never take it.
|
|
442
|
+
|
|
443
|
+
Decimals are derived, not declared:
|
|
444
|
+
|
|
445
|
+
```
|
|
446
|
+
intWidth = max(len(formatInt(min)), len(formatInt(max))) # formatInt includes a leading "-"
|
|
447
|
+
decimals = max(0, width - intWidth - 1) # the -1 is the decimal point
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Derived from `min`/`max` rather than from the current value, so the number of decimals is **stable
|
|
451
|
+
as the value moves** — `9.9` does not become `10` the moment it crosses ten. Worked example, a
|
|
452
|
+
temperature with `min: 0, max: 50, width: 4`: `intWidth` = 2, so `decimals` = 1, giving `30.1`. Give
|
|
453
|
+
the same topic `min: -40, max: 125, width: 5` and `intWidth` = 3, `decimals` = 1, giving `-40.0` and
|
|
454
|
+
`125.0` — both exactly 5 characters.
|
|
455
|
+
|
|
456
|
+
- Right-align in `width` characters with tabular numerals, so digits do not jitter as values update.
|
|
457
|
+
- **Never truncate a number to fit.** A sensor reporting `-999` in a `width: 4` field overflows the
|
|
458
|
+
field rather than rendering `-99`. Overflowing looks broken, which it is; truncating lies.
|
|
459
|
+
- Default when `width` is absent: 1 decimal for `float`, 0 for `int`, matching current practice.
|
|
460
|
+
|
|
461
|
+
> **Naming hazard.** `MqttBar` already has `get width()`, returning the bar fill as a percentage.
|
|
462
|
+
> A schema `width` arriving on `mt` would sit confusingly close to it. Rename the getter to
|
|
463
|
+
> `fillPercent` while doing this work. **D-24**
|
|
464
|
+
|
|
465
|
+
**Out of range.** A reading past its `min`/`max` gets a red value and a filled-to-the-end bar
|
|
466
|
+
rather than an overflowing one. Cheap, and it catches broken sensors. **D-10**
|
|
467
|
+
|
|
468
|
+
## 6. Graphs
|
|
469
|
+
|
|
470
|
+
Not a fourth card mode. The graph icon on a reading (already on `mqtt-bar`) adds that series to the
|
|
471
|
+
**shared graph panel below the grid** — which is the existing behaviour and the right one, because
|
|
472
|
+
the interesting comparison is usually across devices ("both greenhouses, last 3 days"), which an
|
|
473
|
+
in-card graph cannot show.
|
|
474
|
+
|
|
475
|
+
Alternative considered and rejected: graph inside the card. It would make the card tall, fight the
|
|
476
|
+
flip's fixed box, and lose cross-device comparison. **D-11**
|
|
477
|
+
|
|
478
|
+
## 7. Layout, ordering and memory
|
|
479
|
+
|
|
480
|
+
### 7.1 One mechanism, not two
|
|
481
|
+
|
|
482
|
+
The brief asks for free dragging on desktop and reordering on mobile. **Recommendation: an ordered
|
|
483
|
+
list on both**, i.e. drag-to-reorder inside a responsive flow grid, not free XY placement. **D-12**
|
|
484
|
+
|
|
485
|
+
Free XY placement needs fixed-size cards (cards change size when they open), breaks on window
|
|
486
|
+
resize, has no meaning on a one-column phone, and needs a second persisted format. Drag-to-reorder
|
|
487
|
+
gives one interaction model, one storage format (`[nodeId, …]`), and behaves under resize.
|
|
488
|
+
|
|
489
|
+
```
|
|
490
|
+
desktop: grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
491
|
+
a FRONT card spans 2 columns where there is room
|
|
492
|
+
mobile: one column; a FRONT card is simply taller
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### 7.2 Drag mechanics
|
|
496
|
+
|
|
497
|
+
- Pointer Events, not HTML5 drag-and-drop (which is unreliable on Android browsers — the target
|
|
498
|
+
platform).
|
|
499
|
+
- Desktop: grab anywhere on the card header and drag; live gap where it will land.
|
|
500
|
+
- Mobile: long-press to lift, or the always-visible ▲▼ buttons in the card header. The buttons are
|
|
501
|
+
not a fallback nobody uses — on a low-end phone they are more reliable than a long-press. **D-13**
|
|
502
|
+
- Keyboard: focus a card, Space to grab, arrows to move, Space to drop.
|
|
503
|
+
|
|
504
|
+
### 7.3 Persistence — `localStorage`, not a cookie
|
|
505
|
+
|
|
506
|
+
The brief says cookie; **recommend `localStorage`**. A cookie is sent on every HTTP request to the
|
|
507
|
+
server (wasteful where bandwidth is the constraint), is capped near 4 KB, and this state never
|
|
508
|
+
needs to reach the server. `localStorage` is same-origin, synchronous, and ~5 MB. **D-14**
|
|
509
|
+
|
|
510
|
+
```
|
|
511
|
+
key: frugaliot:layout:v1:{org}/{project}
|
|
512
|
+
value: { order: ["esp8266-fb94bb", …], mode: { "esp8266-fb94bb": "front" }, pinned: [] }
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
- New devices appear at the end of the order, never reshuffling what the user arranged.
|
|
516
|
+
- Unknown ids in a stored order are ignored (device retired) rather than erroring.
|
|
517
|
+
- Mode is remembered per device, so returning to a project restores the same open cards.
|
|
518
|
+
- A project with one or two devices opens them on FRONT by default — a grid of one summary card is
|
|
519
|
+
a wasted screen. **D-15**
|
|
520
|
+
- Later, optionally, sync the layout to the logged-in user's account so it follows them across
|
|
521
|
+
devices. Explicitly out of scope for now.
|
|
522
|
+
|
|
523
|
+
## 8. Page chrome
|
|
524
|
+
|
|
525
|
+
```
|
|
526
|
+
┌───────────────────────────────────────────────────────────────┐
|
|
527
|
+
│ Frugal IoT [dev ▾] [lotus ▾] ● connected EN ▾ ⚙ │ sticky
|
|
528
|
+
├───────────────────────────────────────────────────────────────┤
|
|
529
|
+
│ ┌ card ┐ ┌ card ┐ ┌ card ┐ │
|
|
530
|
+
│ ┌ card ┐ ┌ card ┐ │
|
|
531
|
+
├───────────────────────────────────────────────────────────────┤
|
|
532
|
+
│ Graph panel (only once something has been added to it) │
|
|
533
|
+
└───────────────────────────────────────────────────────────────┘
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Org and project selectors already exist on `MqttWrapper`; they move into a compact sticky header.
|
|
537
|
+
The trailing ⚙ is app/admin settings (existing `mqtt-admin`), distinct from a card's own ⚙.
|
|
538
|
+
|
|
539
|
+
### 8.1 States that must be designed, not left to chance
|
|
540
|
+
|
|
541
|
+
| State | Treatment |
|
|
542
|
+
|---|---|
|
|
543
|
+
| No project chosen | centred prompt with the project selector, nothing else |
|
|
544
|
+
| Project chosen, no devices yet | "Waiting for devices…" with a spinner and the broker name |
|
|
545
|
+
| Device discovered, no readings | card in SUMMARY with the name and "no readings yet" |
|
|
546
|
+
| MQTT disconnected | banner across the top, grid dimmed, values frozen not blanked |
|
|
547
|
+
| Device offline | card as §5, still draggable, still openable |
|
|
548
|
+
|
|
549
|
+
## 9. Visual style suggestions
|
|
550
|
+
|
|
551
|
+
Offered because the brief asks for them; all cheap to change later if they are wrong.
|
|
552
|
+
|
|
553
|
+
- **Battery**: on the summary, the level icon alone — the summary is a glanceable view and the level
|
|
554
|
+
is already in the icon's shape, so the number is clutter. The percentage stays on the icon's title
|
|
555
|
+
and appears as text on the front. A bare "47%" beside a device name was the worst of both: it does
|
|
556
|
+
not say what is 47%, and it reads as a humidity or a soil moisture.
|
|
557
|
+
- **Branding**: the project's greens from `frugaliot.naturalinnovation.org` — `#2ecc71` leaf,
|
|
558
|
+
`#27ae60` deeper — used as a **pale tint behind near-black text**, not as a fill under white text
|
|
559
|
+
the way the main site's hero panels do. These screens get read in direct sun and in dim sheds;
|
|
560
|
+
`#1a1a1a` on `#f3faf6` is about 16:1 and survives both, where white on green does not. The green
|
|
561
|
+
appears as the card tint, a thin band under the header, the section rules, and the live status
|
|
562
|
+
dot. **D-42**
|
|
563
|
+
- **Logo**: the icon the main site uses in its own header, top left, linking back to it.
|
|
564
|
+
- **Card**: pale green (`#f3faf6`), 8px radius, 1px hairline border, plus a 1px soft shadow. Not the
|
|
565
|
+
heavy black border of today. (Note: the current CSS writes `border: 1px,black,solid` in several
|
|
566
|
+
places, which is invalid CSS and silently does nothing — worth fixing while here.)
|
|
567
|
+
- **Hierarchy**: the *value* is the loudest thing on the card. Label small, uppercase-ish, gray
|
|
568
|
+
(`#6b6b6b`); value large, near-black, tabular numerals so digits do not jitter as they update.
|
|
569
|
+
These devices get read outdoors on a phone in sunlight — favour contrast over subtlety.
|
|
570
|
+
- **Colour**: use the topic's schema `color` for the bar fill only. Never let colour be the only
|
|
571
|
+
carrier of meaning; the number is always shown.
|
|
572
|
+
- **Bars**: keep them. They are cheap, fast, readable at a glance, and already implemented. Add
|
|
573
|
+
min/max end labels so the value has context.
|
|
574
|
+
- **Touch targets** ≥ 44px. Today's CSS reaches mobile by bumping `font-size` to `xxx-large` at
|
|
575
|
+
`max-width: 1001px`, which is coarse; replace with CSS custom properties for spacing/type scale
|
|
576
|
+
set once per breakpoint.
|
|
577
|
+
- **Back tint**: give the back a faintly different background so it is obvious which side you are
|
|
578
|
+
on without reading it.
|
|
579
|
+
- **Dark mode**: not now, but define colours as custom properties from the start so
|
|
580
|
+
`prefers-color-scheme` is a small diff later.
|
|
581
|
+
- **Motion**: 150–200ms on the summary↔front expand and the flip; respect
|
|
582
|
+
`prefers-reduced-motion` by cutting to the end state.
|
|
583
|
+
|
|
584
|
+
## 10. Permissions — the WRITE capability
|
|
585
|
+
|
|
586
|
+
A new org-scoped `WRITE` capability, required for **any** change: toggling an actuator, editing a
|
|
587
|
+
control's limit, renaming a device, setting a wire. Capabilities are free text in the
|
|
588
|
+
`permissions(id, capability, org)` table, so adding one needs no schema change — only the client's
|
|
589
|
+
dropdown at `webcomponents.js:3060`, which today offers `OTAUPDATE`/`ADMIN`/`READ`. **D-25**
|
|
590
|
+
|
|
591
|
+
The full set after this work — the dropdown must offer all five: **D-31**
|
|
592
|
+
|
|
593
|
+
| Capability | Grants |
|
|
594
|
+
|---|---|
|
|
595
|
+
| `READ` | see an organization's projects and devices at all |
|
|
596
|
+
| `WRITE` | change anything: actuators, control settings, names, wiring |
|
|
597
|
+
| `OTAUPDATE` | upload and manage OTA binaries |
|
|
598
|
+
| `OTAFLASH` | flash a device over USB from the browser |
|
|
599
|
+
| `ADMIN` | permissions, projects, the API tools, raw message publishing |
|
|
600
|
+
|
|
601
|
+
`OTAFLASH` and `OTAUPDATE` are **independent — neither implies the other**, and `OTAFLASH` will be
|
|
602
|
+
the more widely granted of the two: flashing requires physically holding the device, which is its own
|
|
603
|
+
access control, whereas pushing an OTA binary reaches every device in the organization at once.
|
|
604
|
+
|
|
605
|
+
### 10.1 What a user without WRITE sees
|
|
606
|
+
|
|
607
|
+
The proposal was that the whole back is hidden. **Recommendation: show the back, read-only,
|
|
608
|
+
instead.** **D-26**
|
|
609
|
+
|
|
610
|
+
The back is "settings *and background info*" (§3.3) and the background half is read-only by nature —
|
|
611
|
+
device id, last seen, battery, wifi SSID, OTA key, every reading with its min/max. That is exactly
|
|
612
|
+
what someone diagnosing a quiet device needs, and none of it is on the front. Hiding it means a
|
|
613
|
+
read-only user cannot answer "when did it last report, and what is its battery doing" without an
|
|
614
|
+
admin.
|
|
615
|
+
|
|
616
|
+
So each element degrades rather than disappearing, using display forms the codebase already has:
|
|
617
|
+
|
|
618
|
+
| With WRITE | Without WRITE |
|
|
619
|
+
|---|---|
|
|
620
|
+
| actuator toggle on the front | state indicator (`✓` / `✗`, per `trueFalseSymbol`) |
|
|
621
|
+
| `mqtt-text` limit field | the value as text |
|
|
622
|
+
| `mqtt-choosetopic` wiring dropdown | the wired topic's `usableName` as text |
|
|
623
|
+
| device name / description inputs | plain text |
|
|
624
|
+
| ADVANCED: flash, OTA actions | section omitted entirely |
|
|
625
|
+
|
|
626
|
+
The gear icon still appears, because the back still has something to show. If a device ever has
|
|
627
|
+
*nothing* read-only worth showing, omit the gear rather than opening an empty card.
|
|
628
|
+
|
|
629
|
+
### 10.2 What WRITE does not govern
|
|
630
|
+
|
|
631
|
+
Card order, which cards are open, and summary/front choice are **local presentation**, stored in
|
|
632
|
+
`localStorage` (§7.3). They are not writes to the system and stay available to everyone. A read-only
|
|
633
|
+
user can still arrange their own dashboard and drive the graph.
|
|
634
|
+
|
|
635
|
+
### 10.3 This is decluttering, not security — and must be labelled as such
|
|
636
|
+
|
|
637
|
+
Every user of an organization connects to the broker with the **same** credentials: `setClientCredentials`
|
|
638
|
+
sets `username = org` and `password = orgConfig.mqtt_password`, and that password reaches the browser
|
|
639
|
+
inside `/config.json`. So a user without `WRITE` still holds working publish credentials and can
|
|
640
|
+
change anything with any MQTT client.
|
|
641
|
+
|
|
642
|
+
Hiding controls is therefore worth doing — it prevents accidents, and it stops a viewer being offered
|
|
643
|
+
actions that are not theirs — but it must not be described to anyone as a permission boundary. Real
|
|
644
|
+
enforcement needs per-user broker credentials, or a server-side publish proxy that checks `WRITE`.
|
|
645
|
+
Either is a substantial piece of work on the server and broker, and belongs on the deferred list
|
|
646
|
+
rather than in this UI change. **D-27**
|
|
647
|
+
|
|
648
|
+
## 11. The project back — administration
|
|
649
|
+
|
|
650
|
+
The card metaphor extends up a level: the **project** has a front and a back too. Front is the grid
|
|
651
|
+
of device cards; the gear in the project header flips to the back, which is a grid of
|
|
652
|
+
**administration cards**. This is what becomes of the rest of `<mqtt-admin>` — today a
|
|
653
|
+
`tabbed-display` of Dashboard / OTA / Admin / Nodes / API with the project UI buried inside tab 0.
|
|
654
|
+
Tabs become cards, and the Dashboard tab stops being a tab because it *is* the front. **D-28**
|
|
655
|
+
|
|
656
|
+
```
|
|
657
|
+
┌───────────────────────────────────────────────────────────────┐
|
|
658
|
+
│ ⚙ dev / lotus ✕ │
|
|
659
|
+
├───────────────────────────────────────────────────────────────┤
|
|
660
|
+
│ ┌ OTA ─────────┐ ┌ Flash ───────┐ ┌ Publish ─────┐ │
|
|
661
|
+
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
662
|
+
│ ┌ Permissions ─┐ ┌ Projects ────┐ ┌ Nodes ───────┐ │
|
|
663
|
+
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
664
|
+
│ ┌ API ─────────┐ │
|
|
665
|
+
│ └──────────────┘ │
|
|
666
|
+
└───────────────────────────────────────────────────────────────┘
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
Each card is shown only if the user's capabilities allow it:
|
|
670
|
+
|
|
671
|
+
| Card | Capability | Note |
|
|
672
|
+
|---|---|---|
|
|
673
|
+
| Info | none | broker, organization, connection — reference information anyone may want |
|
|
674
|
+
| OTA upload and manage | `OTAUPDATE` | existing OTA tab, unchanged |
|
|
675
|
+
| Flash | `OTAFLASH` | **new capability**; today `mqtt-flash` sits inside the OTA tab and so is gated on `OTAUPDATE` — it moves out |
|
|
676
|
+
| Permissions | `ADMIN` | existing Admin tab, unchanged |
|
|
677
|
+
| Projects | `ADMIN` | existing Admin tab, unchanged |
|
|
678
|
+
| Publish Message | `ADMIN` | ⚠ a **loosening** — see below |
|
|
679
|
+
| Nodes | `ADMIN` | unchanged; `READ` would be defensible, see below |
|
|
680
|
+
| API | `ADMIN` | unchanged — `apiRestContent()` includes a "Node Actions" section that writes, so this is not a read-only tool |
|
|
681
|
+
|
|
682
|
+
**Publish Message is deliberately not on `WRITE`.** A user who can change things through the UI
|
|
683
|
+
should not thereby get a raw publish box — the UI constrains them to topics and values that mean
|
|
684
|
+
something, and an arbitrary publish breaks things. Note it is gated *more* tightly than `ADMIN`
|
|
685
|
+
today: `server_config.user.id !== 1 ? null : …`, i.e. superuser only, with the comment "not really a
|
|
686
|
+
feature, more for testing and debugging". Moving it to `ADMIN` is therefore a widening, deliberate but
|
|
687
|
+
worth seeing plainly. **D-32**
|
|
688
|
+
|
|
689
|
+
**Nodes stays on `ADMIN`.** Its content is genuinely read-only — `nodesRestContent()` is a heading
|
|
690
|
+
and a table — so `READ` would be defensible. But a UI redesign is the wrong vehicle for widening who
|
|
691
|
+
can see an organization's device inventory; if that is wanted it should be its own decision. **D-33**
|
|
692
|
+
|
|
693
|
+
The **Info card is ungated**, and is where the connection details went when they left the header:
|
|
694
|
+
which broker, as which organization, and whether it is connected. Wanted once, not in the corner of
|
|
695
|
+
every screen — and the natural place for more of the same later, bridge status and so on. **D-43**
|
|
696
|
+
|
|
697
|
+
Because Info is always there the back is never empty, so the gear is offered as soon as an
|
|
698
|
+
organization is chosen. D-29's rule — omit the gear rather than open onto nothing — still stands and
|
|
699
|
+
is still implemented; it simply no longer triggers. **D-29**
|
|
700
|
+
|
|
701
|
+
**An admin card has a summary and a back, and no front.** There is nothing to watch at a glance in
|
|
702
|
+
an OTA uploader, so the two-stage open of a device card makes no sense here: the card is its name
|
|
703
|
+
until you open it, and opening it shows the whole function. Seven expanded at once is a wall of
|
|
704
|
+
forms. The content is built on first open and then kept, so moving between cards does not discard a
|
|
705
|
+
half-filled one. **D-41**
|
|
706
|
+
|
|
707
|
+
**API stays as it is.** It is a lightweight surface whose only purpose is developers poking at their
|
|
708
|
+
own API while testing. Wrap the existing markup in a card and change nothing else — no restyle, no
|
|
709
|
+
i18n pass, no responsive work. Spending design effort there is spending it in the wrong place. **D-30**
|
|
710
|
+
|
|
711
|
+
**Flash** is already designed in `FLASH_PLAN.md` and implemented as `mqtt-flash`; it becomes a card
|
|
712
|
+
without redesign. Note it is the one admin function that is *device*-specific as well as
|
|
713
|
+
project-level, so it also earns a link from a device's back under ADVANCED (§3.3).
|
|
714
|
+
|
|
715
|
+
## 12. Out of scope for this design
|
|
716
|
+
|
|
717
|
+
Named so they do not creep in: multi-project dashboards on one screen, per-user server-side layout
|
|
718
|
+
sync, alerts/notifications, dark mode, historical CSV browsing beyond the existing graph panel,
|
|
719
|
+
grouping cards into user-defined sections, and the flash/provisioning UI (`mqtt-flash`, already
|
|
720
|
+
designed in `FLASH_PLAN.md`) beyond a link on the back's ADVANCED section.
|
|
721
|
+
|
|
722
|
+
---
|
|
723
|
+
|
|
724
|
+
## 13. Decisions — all confirmed 2026-08-28
|
|
725
|
+
|
|
726
|
+
D-1 … D-24 are settled as below and are inputs to the implementation plan, not open questions.
|
|
727
|
+
Reopening one means revisiting this document, not deciding it in code.
|
|
728
|
+
|
|
729
|
+
| # | Decision | Resolution |
|
|
730
|
+
|---|---|---|
|
|
731
|
+
| D-1 | Many cards open at once, or accordion? | Many |
|
|
732
|
+
| D-2 | Interactive controls on SUMMARY? | No — whole card is one tap target |
|
|
733
|
+
| D-3 | Actuators tappable on FRONT? | Yes — front acts, back configures. Controls stay read-only on the front |
|
|
734
|
+
| D-4 | Anything still collapsed on BACK? | Only ADVANCED |
|
|
735
|
+
| D-5 | Edit per-reading min/max on BACK? | Yes, where the topic is writable |
|
|
736
|
+
| D-6 | Literal flip on mobile? | No — full-screen sheet; flip on desktop only |
|
|
737
|
+
| D-7 | Summary as format string or twig list? | Twig list, defaulting to module `summaryText()` for now |
|
|
738
|
+
| D-8 | How to disambiguate two same-named readings? | Use the module's `name` ("Soil Temperature"), not "(DS18B20)" |
|
|
739
|
+
| D-9 | `precision:` or `width:` in `topics.yaml`? | `width:`, matching the node; decimals derived from `min`/`max` |
|
|
740
|
+
| D-10 | Out-of-range styling? | Yes, red value + clamped bar |
|
|
741
|
+
| D-11 | Graph in-card or shared panel? | Shared panel below the grid |
|
|
742
|
+
| D-12 | Free XY drag on desktop? | No — drag-to-reorder in a flow grid, both platforms |
|
|
743
|
+
| D-13 | ▲▼ buttons always visible on mobile? | Yes, alongside long-press drag |
|
|
744
|
+
| D-14 | Cookie or `localStorage`? | `localStorage` |
|
|
745
|
+
| D-15 | Auto-open FRONT for small projects? | Yes, ≤2 devices |
|
|
746
|
+
| D-16 | Detect a control module by name prefix or flag? | `climate` deleted ✔; `control: true` still worth adding for robustness |
|
|
747
|
+
| D-17 | Module structure on the FRONT? | None. Flat ordered rows; modules return on the BACK |
|
|
748
|
+
| D-18 | Actuator driven by a control — merge the rows? | No. Separate rows; the override UX is deferred (§14) |
|
|
749
|
+
| D-19 | Control row style on the BACK? | `[>] [ 32.0 ] ± [ 3.0 ]`, per `dashboard_example.html` |
|
|
750
|
+
| D-20 | Replace hand-coded `summaryText()`? | Yes, eventually — module-level `summary: [leaves]`, same mechanism |
|
|
751
|
+
| D-21 | Who declares FRONT content? | The device, as an ordered list of twigs and control-module ids |
|
|
752
|
+
| D-22 | Default FRONT when undeclared? | `graphable` readings, then actuators, then controls |
|
|
753
|
+
| D-23 | Units in the `languages` table or their own? | Their own table — `el()`'s filters actively reject `°C`, `%RH`, `m/s` |
|
|
754
|
+
| D-24 | Rename `MqttBar.get width()`? | Yes → `fillPercent`, to free the name for the schema's `width` |
|
|
755
|
+
| D-25 | Add a `WRITE` capability? | Yes, org-scoped, required for every change including an actuator tap |
|
|
756
|
+
| D-26 | Hide the whole back without WRITE? | No — show it read-only; omit only ADVANCED's actions |
|
|
757
|
+
| D-27 | Is client-side WRITE gating a security boundary? | No, and must not be described as one — all org users share one broker credential |
|
|
758
|
+
| D-28 | What becomes of `<mqtt-admin>`'s other tabs? | Cards on the project's back, reached by the project gear |
|
|
759
|
+
| D-29 | Project gear when the user can do nothing? | Omit the gear rather than open an empty back |
|
|
760
|
+
| D-30 | Redesign the API tab? | No — wrap the existing markup in a card, change nothing |
|
|
761
|
+
| D-31 | Capability dropdown contents | Add `WRITE` and `OTAFLASH` to the existing `OTAUPDATE`/`ADMIN`/`READ` |
|
|
762
|
+
| D-32 | Publish Message gating | `ADMIN` — not `WRITE`; note this widens it from today's superuser-only |
|
|
763
|
+
| D-33 | Nodes card gating | `ADMIN`, as today — do not widen access as a side effect of a redesign |
|
|
764
|
+
| D-34 | `devices.yaml` key matching | device id, then exact OTA key, then longest prefix of the OTA key |
|
|
765
|
+
| D-35 | Which topics need `width`? | `float` and `exponential` only — 25 topics; ints derive their field width from `min`/`max` |
|
|
766
|
+
| D-36 | Is a device `summary:` list required? | No — defaults to the first four entries of `front:`; a declared list is uncapped |
|
|
767
|
+
| D-37 | Module summary flag polarity | `summary: false` to opt out — opt-in would have flagged 23 of 33 modules |
|
|
768
|
+
| D-38 | A control on the summary line | A chip (`Relay ✓`) via `summaryShort()`, not its whole rule |
|
|
769
|
+
| D-39 | Battery on the summary line | The level icon alone; the percentage lives on its title and on the front |
|
|
770
|
+
| D-40 | An unwired control on the summary | Omitted — it is doing nothing. Still shown on the front |
|
|
771
|
+
| D-41 | Do admin cards have a front? | No — summary and back only; collapsed to a name until opened |
|
|
772
|
+
| D-42 | How the brand greens are used | A pale tint behind near-black text, never a fill under white — sunlight and dim sheds |
|
|
773
|
+
| D-43 | Where the connection details live | An ungated Info card on the project's back, not an expandable in the header |
|
|
774
|
+
| D-44 | How a module without a hand-written summary gets one | A generic default — its own readings, capped at two — not a list per module |
|
|
775
|
+
|
|
776
|
+
---
|
|
777
|
+
|
|
778
|
+
## 14. Deferred to a later step
|
|
779
|
+
|
|
780
|
+
Intended, but explicitly **after** the card UI switch is complete. Distinct from §12, which is not
|
|
781
|
+
planned at all. This list is expected to grow as the plan and the implementation turn things up.
|
|
782
|
+
|
|
783
|
+
| # | Deferred | Why it waits |
|
|
784
|
+
|---|---|---|
|
|
785
|
+
| L-1 | **Overriding a control-driven actuator** — tapping a relay a control loop is driving | Has its own UX thinking in progress; depends on L-2. Interim behaviour: the loop overwrites the tap (D-18) |
|
|
786
|
+
| L-2 | **Setting `manual` from the UI** | Node side exists (`Control_Sonoff`), UX side does not. Needs a reverse lookup `mt.drivenBy` — for an actuator, the control whose `out` is wired to it. Note `out.wired` holds a *set* path, so the comparison must normalise as `findTopic` does |
|
|
787
|
+
| ~~L-4~~ | ~~`control: true` in `modules.yaml`~~ | **Not doing.** Control modules start with "control"; if that ever stops being true, add the flag then |
|
|
788
|
+
| ~~L-5~~ | ~~`TODO-213` — a module declaring dynamically-added outputs~~ | **Already supported.** What remains is merging `manual` into `controlhysteresis` — non-trivial, not urgent, and part of L-1/L-2 |
|
|
789
|
+
| L-6 | **Per-user server-side layout sync** (§7.3) | **Unlikely.** It would mean server-side database changes to hold user preferences, which is not a direction worth taking for this; `localStorage` covers the common case |
|
|
790
|
+
| L-7 | **Real enforcement of `WRITE`** (§10.3) | A known, separate project — per-user broker credentials or a server-side publish proxy. Until it lands, the UI gating is decluttering only and must be described that way |
|
|
791
|
+
|
|
792
|
+
---
|
|
793
|
+
|
|
794
|
+
## 15. Open issues for the implementation plan
|
|
795
|
+
|
|
796
|
+
Not UX decisions — things the plan has to answer. **All resolved 2026-08-28**; recorded here with
|
|
797
|
+
the reasoning because the plan depends on them.
|
|
798
|
+
|
|
799
|
+
### O-1 Where does the per-device `front:` / `summary:` config actually live? — RESOLVED
|
|
800
|
+
|
|
801
|
+
§4 says "org/project config, or a new `devices.yaml`" and that is not good enough to build from.
|
|
802
|
+
What the code says today:
|
|
803
|
+
|
|
804
|
+
- The org yamls hold **no** per-node config at all — `winam.yaml` is three lines.
|
|
805
|
+
- `p.nodes` in `/config.json` is built dynamically by `addLoggedNodesToConfig()` from
|
|
806
|
+
`mqttLogger.reportNodes()`, i.e. `{nodeid: lastseen}`. There is nowhere static to hang a layout.
|
|
807
|
+
- The **OTA key is a firmware-build identifier**, not a secret: binaries live at
|
|
808
|
+
`ota/<org>/<project>/<key>` (e.g. `ota/dev/lotus/sonoff_r4`), the device publishes it as
|
|
809
|
+
`ota/key`, and the admin form labels the field "OTA Key or Device ID".
|
|
810
|
+
|
|
811
|
+
**Resolution: a new `config.d/schema/devices.yaml`, keyed by OTA key** — sitting alongside
|
|
812
|
+
`modules.yaml` and `topics.yaml`. Keys match by **prefix**, so one entry covers an application across
|
|
813
|
+
every board it is built for; see §4.6 for the resolution order. Served in `/config.json` next to
|
|
814
|
+
`schema.modules` and `schema.topics`; the client reads `ota/key` off MQTT and looks the layout up.
|
|
815
|
+
This is exactly the brief's "a table per OTA-key" instinct, and it means **zero per-device admin** —
|
|
816
|
+
every device flashed with `sonoff_r4` gets the same cards. Server cost is small: load one more YAML
|
|
817
|
+
into `config.schema`.
|
|
818
|
+
|
|
819
|
+
*Rejected alternative:* the device declares its own layout in its discovery message. It puts a purely
|
|
820
|
+
presentational preference on the most constrained side of the system, costs flash and MQTT bytes, and
|
|
821
|
+
makes restyling a card require a reflash.
|
|
822
|
+
|
|
823
|
+
### O-2 Does the card use Shadow DOM? — RESOLVED, and neither fork is needed
|
|
824
|
+
|
|
825
|
+
The concern was that `html-element-extended` presumes Shadow DOM, so light-DOM cards would mean
|
|
826
|
+
either an option flag in a library other sites depend on, or a second version kept in sync. Reading
|
|
827
|
+
the library, neither is necessary.
|
|
828
|
+
|
|
829
|
+
**`HTMLElementExtendedMinimum` already exists, is already exported, and does not attach a shadow
|
|
830
|
+
root.** The shadow root is attached in exactly one place — the `HTMLElementExtended` subclass
|
|
831
|
+
constructor:
|
|
832
|
+
|
|
833
|
+
```js
|
|
834
|
+
class HTMLElementExtended extends HTMLElementExtendedMinimum {
|
|
835
|
+
constructor(props) { super(props); this.attachShadow({ mode: 'open' }); }
|
|
836
|
+
}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
The only thing stopping the Minimum class from rendering is that `renderAndReplace()` names
|
|
840
|
+
`this.shadowRoot` unconditionally — in **three places, all inside that one method** (lines 295, 297,
|
|
841
|
+
301). Change them to `(this.shadowRoot || this)` and the library supports both, with no flag and no
|
|
842
|
+
fork.
|
|
843
|
+
|
|
844
|
+
**It is backwards compatible by construction:** every existing consumer extends
|
|
845
|
+
`HTMLElementExtended`, so `this.shadowRoot` is truthy and behaviour is byte-identical. Nothing in
|
|
846
|
+
`webcomponents.js` references `shadowRoot` at all, so the client side is free too.
|
|
847
|
+
|
|
848
|
+
So, per element:
|
|
849
|
+
|
|
850
|
+
| Element | Base | Stylesheet |
|
|
851
|
+
|---|---|---|
|
|
852
|
+
| `mqtt-devicecard`, `mqtt-devicegrid` | `HTMLElementExtendedMinimum` — light DOM | `frugaliot.css` on the page; **no `CssUrl` link node** |
|
|
853
|
+
| `mqtt-bar`, `mqtt-text`, `mqtt-toggle`, … | `HTMLElementExtended` — unchanged | as today |
|
|
854
|
+
|
|
855
|
+
That also removes, for the new elements, the "first node rendered is a link to a common stylesheet"
|
|
856
|
+
boilerplate — there are 18 such `href: CssUrl` nodes in `webcomponents.js` today.
|
|
857
|
+
|
|
858
|
+
**Correcting my earlier framing:** I implied drag-and-drop, CSS grid and a fixed-position sheet need
|
|
859
|
+
light DOM. They do not — pointer events cross shadow boundaries fine with `composedPath()`, and
|
|
860
|
+
`position: fixed` works inside a shadow root. The honest arguments are styling ergonomics (the notes
|
|
861
|
+
at the top of `frugaliot.css` are a standing record of the `:host-context` cost) and not duplicating
|
|
862
|
+
a stylesheet into every root. Worth being straight about, since this touches a shared library.
|
|
863
|
+
|
|
864
|
+
**Two plan consequences.** The change lands in
|
|
865
|
+
`~/git/github_mitra42/html-element-extended` — a repo outside the frugal-iot tree, to be called out
|
|
866
|
+
when edited. And the client depends on the *published* package (`^0.1.6`; the local repo and
|
|
867
|
+
`node_modules` copy are currently identical), so it needs a version bump and publish, or an `npm
|
|
868
|
+
link` while developing.
|
|
869
|
+
|
|
870
|
+
*Optional follow-up, not required:* the widgets that keep Shadow DOM could share one constructible
|
|
871
|
+
stylesheet via `adoptedStyleSheets` instead of a `<link>` per root — twelve devices is on the order of
|
|
872
|
+
80 roots each parsing the same 10 KB. Needs a `<link>` fallback for Safari before 16.4.
|
|
873
|
+
|
|
874
|
+
### O-3 Cutover, or parallel? — RESOLVED: a parallel page, not a flag
|
|
875
|
+
|
|
876
|
+
**A new page alongside `index.html`**, not a `layout=` attribute on `mqtt-project`. `index.html`
|
|
877
|
+
retires once the cards cover everything it does — the admin functions especially.
|
|
878
|
+
|
|
879
|
+
Worth being precise about what `index.html` currently *is*, because it is not the project UI: its
|
|
880
|
+
body is a single `<mqtt-admin>`, whose render puts a `tabbed-display` with tabs
|
|
881
|
+
Dashboard / OTA / Admin / Nodes / API, and tab 0 "Dashboard" contains the `<mqtt-wrapper>`. So the
|
|
882
|
+
project UI is nested inside the admin element, and the four non-Dashboard tabs are exactly the
|
|
883
|
+
functions that must be covered before `index.html` can go.
|
|
884
|
+
|
|
885
|
+
This is simpler than the flag: no branch inside `mqtt-project`, and the old path stays untouched
|
|
886
|
+
rather than modified, so there is nothing to regress while the cards are being built.
|
|
887
|
+
|
|
888
|
+
### O-8 Where does the graph panel go on a phone? — RESOLVED: try the sheet
|
|
889
|
+
|
|
890
|
+
**What a "sheet" is** (it appears in §3.4 for the card back too): a panel that slides up from the
|
|
891
|
+
bottom edge and sits *over* the page, which dims behind it. It covers part or all of the screen, and
|
|
892
|
+
is dismissed by swiping it down, tapping the dimmed area, or a ✕. Every mobile OS uses them — the
|
|
893
|
+
iOS share sheet, an Android "open with" chooser — so the gesture needs no teaching. The reason it
|
|
894
|
+
suits us: it appears *where the thumb already is*, regardless of how far down the page you had
|
|
895
|
+
scrolled, which is precisely the "graph is eight cards below the bar you just tapped" problem.
|
|
896
|
+
|
|
897
|
+
**Resolution: on mobile the graph opens as a sheet; on desktop it stays a panel below the grid.**
|
|
898
|
+
Same mechanism as the card back, so it is built once. If it proves wrong in the hand it is a
|
|
899
|
+
contained change — the graph panel keeps working as a panel, so falling back costs a media query.
|
|
900
|
+
|
|
901
|
+
### O-4 Is the card a new element, or a rewrite of `MqttNode`?
|
|
902
|
+
|
|
903
|
+
New element, `mqtt-devicecard`, bound to the same `MqttTopicNode` through the existing
|
|
904
|
+
`el.mt` / `mt.element` convention, extending `HTMLElementExtendedMinimum` per O-2. `MqttNode` and
|
|
905
|
+
`MqttGroup` are not modified at all — with O-3 putting the cards on their own page, the two never
|
|
906
|
+
coexist in one document, so `mt.element` has a single owner and the old path cannot regress while the
|
|
907
|
+
new one is built.
|
|
908
|
+
|
|
909
|
+
### O-5 How is this developed and reviewed without hardware? — RESOLVED: yes, step 0
|
|
910
|
+
|
|
911
|
+
There is no test infrastructure — `npm test` is `echo "Error: no test specified"`. **A mock harness is
|
|
912
|
+
step 0 of the plan**: a `mock.js` that replays canned discovery and value messages into the data tree
|
|
913
|
+
with no broker, so every state can be put on screen in seconds — offline, stale, no readings yet,
|
|
914
|
+
out-of-range, control wired and unwired, one device, twelve devices. Cards cannot be built honestly
|
|
915
|
+
against a live broker that only shows the two or three states that happen to occur while watching.
|
|
916
|
+
This is the highest-leverage item in the plan and belongs before any card code.
|
|
917
|
+
|
|
918
|
+
**It must cover the existing UI, not just the cards.** The same canned message set drives
|
|
919
|
+
`mqtt-wrapper` → `MqttNode` → `MqttGroup` and the new card page, so the old UX has a regression check
|
|
920
|
+
for the whole of the build rather than being verified by hand at the end. Two consequences for how it
|
|
921
|
+
is built:
|
|
922
|
+
|
|
923
|
+
- The mock feeds the **data tree**, not the card, so it is layout-agnostic by construction — the
|
|
924
|
+
seam is `mqtt_subscribe`/`message_received`, below both UIs.
|
|
925
|
+
- Assertions need to be about rendered output, not internals, or they will not survive the refactor
|
|
926
|
+
they exist to protect. Snapshot the text content and key attributes of a node's rendered tree for a
|
|
927
|
+
given message set; a diff is then either an intended change or a regression.
|
|
928
|
+
|
|
929
|
+
This also makes the file split in the appendix safe: run the snapshots before and after, and a purely
|
|
930
|
+
mechanical split has to produce identical output.
|
|
931
|
+
|
|
932
|
+
### O-6 Who fills in `width:` and `units:` across 54 topics?
|
|
933
|
+
|
|
934
|
+
Mechanical, but needs per-sensor judgement, so it is its own pass in the server schema, done *before*
|
|
935
|
+
the cards land — the cards look wrong without it, though they will not break (defaults exist).
|
|
936
|
+
Extend `check-schema.js` to warn on any displayable topic missing either.
|
|
937
|
+
|
|
938
|
+
### O-7 Relative time in four languages
|
|
939
|
+
|
|
940
|
+
"2h ago", "3 days ago" and their plurals would mean a pile of new `languages` entries. Use
|
|
941
|
+
`Intl.RelativeTimeFormat` instead — available in every target browser, needs no translation entries,
|
|
942
|
+
and gets plural rules right in HI and ID where hand-rolling would not.
|
|
943
|
+
|
|
944
|
+
### O-9 `index-embedded.html` is a third consumer, and constrains the split
|
|
945
|
+
|
|
946
|
+
It uses a bare `mqtt-client` plus `mqtt-bar` / `mqtt-toggle` with explicit topic paths — no
|
|
947
|
+
`mqtt-wrapper`, no project tree, no `server_config`. So `core.js` + `widgets.js` must work standalone,
|
|
948
|
+
which the §Appendix split allows but must not regress. Separately: `index.html` links
|
|
949
|
+
`/manifest.json`, which lives in the *server's* `public/`, so the client directory is not
|
|
950
|
+
self-serving.
|
|
951
|
+
|
|
952
|
+
### O-10 Discovery order versus stored order
|
|
953
|
+
|
|
954
|
+
Cards arrive asynchronously as discovery messages land, so the stored order has to be applied per
|
|
955
|
+
arrival, not once at load, and a device with no stored position and no name yet still needs a stable
|
|
956
|
+
slot. Append in arrival order; re-sort only on an explicit drag. Unplanned, this is where the grid
|
|
957
|
+
flickers.
|
|
958
|
+
|
|
959
|
+
---
|
|
960
|
+
|
|
961
|
+
## Appendix — implementation notes parked for step 2
|
|
962
|
+
|
|
963
|
+
Not a plan; a scratchpad so nothing is lost while the UX is still moving.
|
|
964
|
+
|
|
965
|
+
**Schema changes.** The master copy is `../frugal-iot-server/config.d/schema/`. Edit there first,
|
|
966
|
+
then propagate outwards to the `../frugal-iot-logger/examples/*/config.d/schema/` copies — never the
|
|
967
|
+
other way round.
|
|
968
|
+
- `modules.yaml`: module-level `summary: true` now, `summary: [leaves]` later (D-20);
|
|
969
|
+
module-level `control: true` (D-16). **No `front:` key here** — it would duplicate `display:`.
|
|
970
|
+
- `topics.yaml`: `width:` (D-9); fill in missing `units:` (D-23).
|
|
971
|
+
- Device/project level (org yaml, or a new `devices.yaml`): `summary: [twigs]`,
|
|
972
|
+
`front: [twigs and control-module ids]`, optional per-entry `label:`.
|
|
973
|
+
- `scripts/check-schema.js` in the logger should learn the new keys.
|
|
974
|
+
|
|
975
|
+
**Splitting `webcomponents.js`** (5.7k lines, one file). Candidate split, as a mechanical
|
|
976
|
+
no-behaviour-change commit *before* any card work:
|
|
977
|
+
- `core.js` — mqtt connection & subscriptions, `MqttTopic`/`MqttTopicProject`/`Node`/`Group` data
|
|
978
|
+
tree, `server_config`, `el()`, i18n, `Watchdog`, helpers. Everything a headless dashboard needs.
|
|
979
|
+
- `widgets.js` — `MqttBar`, `MqttText`, `MqttToggle`, `MqttGauge`, `MqttSlider`, `MqttColor`,
|
|
980
|
+
`MqttChooseTopic`, `MqttGraph`, `MqttGraphDataset`. Shared by all UIs including
|
|
981
|
+
`dashboard_example.html`.
|
|
982
|
+
- `cards.js` — new: `mqtt-devicecard`, `mqtt-devicegrid`, layout store.
|
|
983
|
+
- `admin.js` — `MqttAdmin`, `MqttFlash`, `MqttLogin`, `TabbedDisplay`.
|
|
984
|
+
- `index.js` — the entry point `index.html` loads; imports the above and defines nothing.
|
|
985
|
+
- `dashboard_example.html` then imports `core.js` + `widgets.js` only, which is the separation the
|
|
986
|
+
brief asks for.
|
|
987
|
+
|
|
988
|
+
**Card ↔ data tree.** The card is a DOM element bound to an `MqttTopicNode`, following the existing
|
|
989
|
+
convention (`el.mt` / `mt.element`, pre-bind before `appendChild`). `MqttGroup` keeps its role as
|
|
990
|
+
the roll-up/`summaryText()` holder but stops rendering `<details>`; the card decides layout and asks
|
|
991
|
+
groups for their summary strings. `MqttNode.render()` is replaced by the card.
|
|
992
|
+
|
|
993
|
+
**Reuse, do not rewrite**: `summaryText()` per module, `mqtt-bar`/`mqtt-text`/`mqtt-toggle`,
|
|
994
|
+
`mqtt-choosetopic` for wiring on the back, `Watchdog` for online/stale/offline, the graph panel. The
|
|
995
|
+
control row on the back is already built in `dashboard_example.html`'s `wireUpDashboard` — lift it
|
|
996
|
+
rather than rewriting it (D-19).
|
|
997
|
+
|
|
998
|
+
**New getters needed on the data tree** (per the house rule that derived values are getters on
|
|
999
|
+
`MqttTopic`, not dashboard logic):
|
|
1000
|
+
- `mt.formatted` — the value rendered per `width` (D-9) plus its unit symbol (D-23).
|
|
1001
|
+
- `mt.frontRows` / `mt.summaryChips` on the node topic — the resolved ordered lists from §4.1/§4.2,
|
|
1002
|
+
so the card reads a list rather than re-deriving the rules.
|
|
1003
|
+
- `nodeMt.isControlModule(groupId)` — reading `control: true` rather than the name prefix, in both
|
|
1004
|
+
`MqttTopic.controlGroups` and the `frugaliot:controlgroup` dispatch in
|
|
1005
|
+
`MqttTopicNode.addGroupFromTemplate`. Deferred as L-4, so the prefix test stands for now.
|
|
1006
|
+
|
|
1007
|
+
**`manual` — node side exists, UX side does not.** The node implements it: `examples/sonoff/sonoff.ino`
|
|
1008
|
+
defines `Control_Sonoff extends Control_Hysteresis` with an extra `manual` OUTbool, where cycling the
|
|
1009
|
+
output sets manual and a long button press drops out of it. `topics.yaml` defines the topic and
|
|
1010
|
+
`MqttGroupControlHysteresis` observes it and renders "Manual". What is missing is any UX for *setting*
|
|
1011
|
+
it.
|
|
1012
|
+
|
|
1013
|
+
Note the `TODO-213` on that line — "handle case of valid topics but not in a module" — which is
|
|
1014
|
+
exactly why `manual` is in `topics.yaml` while no module declares it: `Control_Sonoff` adds the output
|
|
1015
|
+
dynamically in a subclass, so there is no module template for the schema to describe. Worth resolving
|
|
1016
|
+
alongside `control: true` (D-16), since both are about a module declaring what it actually publishes.
|
|
1017
|
+
|
|
1018
|
+
Setting `manual` from the UI is **deferred** (§14), so nothing above blocks the card work.
|
|
1019
|
+
|
|
1020
|
+
**Open technical questions**: whether cards keep Shadow DOM (styling across shadow boundaries is
|
|
1021
|
+
already awkward — see the notes at the top of `frugaliot.css`); whether the layout store belongs in
|
|
1022
|
+
`core.js` or `cards.js`; how the mobile sheet coexists with `position: sticky` header.
|