Draw the panel as a panel

Five things a wall panel showed that a wall panel should not.

**A tile's body no longer clips.** It scrolled, and a box that scrolls
also cuts whatever crosses its edge — which took the glow off a lit
button at exactly the width where the button filled its tile, and off a
gauge's arc at exactly the height where the dial filled its own. Only
what is written or listed asks for a scroller now; everything else is a
picture drawn to fit, and what overflows is left to the frame, which
clips at the tile's edge where a shadow has already faded out. The
slider's phantom scrollbar goes with it.

**The selector is a selector.** Named for what it does rather than what
it is, and the choice it is holding is held in the dashboard's own
primary — a pill that slides between the options rather than a grey one
that had to be looked for. The stored type is untouched, so no document
changes meaning.

**The arrangement is held off the panel's edges**, by the same distance
it holds between two widgets. The ground is not held off with it: a
background covers the whole panel, and only what is arranged on it has
a margin. No stored panel loses a row to it.

**The rail is drawn on the panel.** It was chrome bolted to the edge of
the screen beside the canvas — in the app's own design rather than the
dashboard's, and on a scaled canvas not even lined up with it. It now
takes a column out of the canvas the way the margin does, scaled with
it and wearing its look. Which cell each widget sits in is unchanged;
only how big a cell is.

Two of these were the same mistake twice: an unlayered rule stating
`position` for everything wearing a class — `.gl-surface` on a rail
placed by a utility, and a blanket lift over every child of a pressable
on a pill placed by `layoutId`. Both now say it one element at a time.
This commit is contained in:
2026-08-24 10:44:53 +02:00
parent fea57064f9
commit 6238728dce
18 changed files with 389 additions and 173 deletions
+13 -68
View File
@@ -12,8 +12,11 @@ Deferring because out of scope is fine, but don't mention deferring than.
### To be sorted
- FEAT/UI: we promise testing, but currently don't provide an UI for testing e.g. mock values or probing edge cases of a flow. This should be resolved (in a dedicated session); I'm thinking of a "Labs" page, which allows simulating an installation with all the flows (using their draft states) and which allows injecting values or mocking values based on events in the past
- FEAT/UI: check if PWA (https://whatpwacando.today/) notifications could be used to have a panel sending notifications to the device event bus (or generally using PWA to retrieve e.g. location etc). We could introduce a general concept of having a panel (a device, like a wall panel or a phone where the pwa (dashboard) runs) being effectively a node with various outputs. Then various inputs could trigger actions like authentification (i.e. you get home and get a notification which allows you to authenticate the door unlock), get notified on alarms (native alarm connector) or to query geolocation (check where the user is before turning of all lights) etc
- FEAT/UI when a dashboard has more widgets than the canvas allows for, the canvas should become scrollable (temporarily) to allow scrolling to the widgets outside the canvas and reordering them
- BUG/UI some nodes (especially smaller ones) in the brain graph still don't have a pixel perfect centered ring and node background
- FEAT/UI: make the "Settings" button in the dashboard panel a toggle; such that it becomes stateful and pressing again closes the settings panel
- FEAT/UI: we promise testing, but currently don't provide an UI for testing e.g. mock values or probing edge cases of a flow. I'm thinking of a "Labs" page, which allows simulating an installation with all the flows (using their draft states) and which allows injecting values or mocking values based on events in the past. This should then use the draft version of flows / dashboards. For the flows we could make a tab-style view and re-embedd the flow canvas. For the dashboards we could reuse the icon rail
- FEAT/UI: check if PWA (https://whatpwacando.today/) notifications could be used to have a panel sending notifications to the device event bus (or generally using PWA to retrieve e.g. location etc). We could introduce a general concept of having a panel (a device, like a wall panel or a phone where the pwa (dashboard) runs) being effectively a node with various outputs. Then various inputs could trigger actions like authentification (i.e. you get home and get a notification which allows you to authenticate the door unlock), get notified on alarms (native alarm connector) or to query geolocation (check where the user is before turning of all lights) etc. Another example would be a wall panel with speech in- and output; we could use pwa as an interface here for any tts/stt instance managed through fluksio
- BUG/UI sync the theme state between panels and installations
- CHORE/UI: a dashboard forced to one theme inside a shell on the other still
matches `dark:` utilities, because the variant is `&:is(.dark *)` and the
@@ -32,6 +35,10 @@ Deferring because out of scope is fine, but don't mention deferring than.
(`ui/segmented.tsx`, `Common/RangePicker.tsx`, and the flow screens). The
dashboard no longer shares them — it has its own, one per look — so
unifying the remaining three is now purely an app-side job.
- CHORE/UI: a panel's rail runs the full height of the canvas whatever it
carries, so two dashboards leave most of a tall pill empty. Hugging its
contents and centring would read better; the column it reserves stays the
same either way.
- CHORE/UI: the house panels are laid out for 1280x800 — twelve columns, twelve
rows. A chart's fixed chrome is now its legend alone: the range picker moved
to a column down the right-hand edge and gave its row back, so a third chart
@@ -209,30 +216,16 @@ Deferring because out of scope is fine, but don't mention deferring than.
- FEAT/UI (deferred until MCP lands): add a "bot" icon button to the home view (graph panel) which opens a chat window (reuse general concept of a side panel like in flows/nodes to make it a chat panel which can open on any screen (stacks below any other existing panel -> introduce stacking) to give support on errors/write code, generate dashboards etc) to explain the error(s)
- FEAT/UI make the header (Fluksio - YEAR) and the logo in the sidebar link to the main page (fluksio.com)
- FEAT/UI consider adding a diagram to the Home view which shows a histogram of the different classes of nodes and which time it takes to execute (logarithmic scale); this should give a hint on the load and help to detect bottle necks/hotspots
- CHORE/API: revoking an OAuth client does not invalidate access tokens already issued; they are stateless JWTs valid up to `MCP_TOKEN_EXPIRE_MINUTES`. Immediate revocation means `app/mcp/http.py` checking the client row still exists.
- CHORE/FLOW: `Pipeline.trigger`'s docstring says a paused flow still publishes so the value shows on the canvas. True only without a queue; with one the item parks before `apply_outputs` and nothing shows. Docstring and behaviour disagree.
### Persistence and databases
From the 2026-08 database review. Verdict recorded under Deferred: the
Postgres + Redis + git-files split stays; the actionable part is durability.
- CHORE/INFRA: Redis AOF runs at `appendfsync everysec`, so up to ~1 s of journaled work-queue entries can vanish on a crash — softer than "journaled before it runs" reads. Queue write volume is low, so `appendfsync always` is likely affordable; otherwise document the loss window.
- CHORE/INFRA: Redis has no auth (`requirepass` unset). Fine on the compose-internal network; a blocker for M5 remote workers, which turn Redis into a network-exposed shared bus.
### Connector write paths
Needs someone watching the real hardware, so it is not a background task. This
is what M4 still waits on, together with porting the flows.
Art-Net can write now: `ConnectorNode.write` carries a node's input ports, a
per-port `channels` map places each on its own DMX channel, and `transmit`
still gates the socket. Verified on the wire against a listener (channel 33 =
255, channel 31 = 60, nothing else set) and the MQTT half was driven end to
end against the house broker. The rig was the `house_control` flow and its
dashboard; it has been deleted now that `dmx` owns the universe, and
`make -C app seed-house` rebuilds it if that verification is ever wanted
again.
- FEAT/NODE: Art-Net against the real fixtures is still untried. The house's own dmxnet sender re-emits universe 1 every 1000 ms, so fluksio and Node-RED overwrite each other; the test needs Node-RED's Art-Net sender stopped, and while it is stopped every channel fluksio does not set is dark.
- CHORE/NODE: the operatorId worry was unfounded — the reference Node-RED `setstat` node for this unit is configured with an empty operatorId and deviceId, so a command needs no registration. The second unit may still differ.
- PERF/NODE: a `wfrac` command is two round trips (read, then set) on the scheduler's thread, so at the default timeout a command can hold a cascade for several seconds. Fine for a person pressing a button; a flow commanding it on a schedule would want the work off that thread.
- CHORE/NODE: `wfrac` writes carry the unit's whole state, so two flows commanding one unit will each undo whatever the other set between their read and their write. One writer per unit, the same rule Art-Net has for a universe.
@@ -243,26 +236,11 @@ again.
### Porting the Node-RED flows
Built: `scripts/tinyhouse/`, `make -C app seed-tinyhouse`. Nineteen flows, 109
nodes, three dashboards, against the reference's 865. What each requirement
became, what was deliberately dropped, and the cutover order are in
`docs/private/node-red-transition.md`. What is left here is what the seeding
does not settle.
- CHORE/FLOW: the DMX channel collisions were decided rather than ported — ch 9
is one bathroom fixture, and `light/traverseAmbientLight` (28-30, colliding
with the window opener's 28-29) is left out on the assumption that it is
decommissioned, since the live scene engine never drove it. The stray 1CH
mapping on 129 beside the awning's 129-130 is dropped. **Check the wiring
before `dmx` transmits.**
- CHORE/FLOW: 1CH values are still used raw, so those fixtures still never go
above 100/255, and the 4CH master channel still takes `v` on a different
scale from its colour channels. Both are `scale` and `master_raw` settings on
the encoders now, so fixing one is a decision about one fixture rather than a
surprise across all of them.
- CHORE/FLOW: `artnet.baseline` in `house.json` is empty. A frame carries the
whole universe, so the first one this node sends darkens every channel it is
not driving. Fill it in from what the fixtures are at, before transmitting.
- FEAT/FLOW: the deferred half of the port — the media plug and the radio at
the media plug's address, the alarm clock, audio through `thgui/display/audio`, and the
fire alarm. The alarm's hook exists: every arbiter takes a `force_at` pulse,
@@ -280,10 +258,7 @@ does not settle.
nodes speak to the house broker here. Either a shared client registry or a
correction to the sentence.
### Bugs found while building the screens
- CHORE/API: revoking an OAuth client does not invalidate access tokens already issued; they are stateless JWTs valid up to `MCP_TOKEN_EXPIRE_MINUTES`. Immediate revocation means `app/mcp/http.py` checking the client row still exists.
- CHORE/FLOW: `Pipeline.trigger`'s docstring says a paused flow still publishes so the value shows on the canvas. True only without a queue; with one the item parks before `apply_outputs` and nothing shows. Docstring and behaviour disagree.
### Out-of-process nodes and modules
@@ -307,32 +282,7 @@ does not settle.
- PERF/API: `queue.stats()` does a keyspace `scan_iter` on every call while two endpoints poll it.
- CHORE/INFRA: dev only — memory-queue ids (`mem-{seq}`) restart at 0 each boot and `FlowRun.id` is the primary key, so a restart without Redis upserts over the previous boot's run rows.
### Wall-panel parity with the current home dashboard
What a fluksio dashboard still lacks to replace `geli-dash` (Dash/Plotly, e-ink
panel: clock and nav chrome, indoor climate, weather forecast strip, calendar
agenda, room light groups, sliders, power/battery bars, and three pages of
InfluxDB time series). Component-level only; the arrangement and the styling are
this design system's business, not that one's.
Decisions taken up front, because most items below depend on them:
- Structured data reaches a widget as a *declared shape*, not as opaque JSON with
a path per binding. A path would leave the picker with nothing to offer and
`widgetIssue` unable to judge a tile from the document alone.
- BUG/UI double check that this aligns with the new data-science pipeline feature
- A chart asks a flow for its series the way every other input widget speaks:
it publishes a request message and reads the answer. No query API, no
database knowledge in the widget.
- Database nodes are transport and credentials only. The InfluxDB node runs the
Flux it is handed and echoes back every other field of the request; building
the query and shaping the answer are Python nodes either side of it. That is
what keeps a widget ignorant of the database, and it is also what a series
read mode inside the node would have prevented. A "grouped nodes" concept
could later package the standard chart→build→db→parse→chart quintet so a
dashboard is not five nodes of wiring each time.
- Nothing e-ink-specific in the widgets. Panel access is a credential problem
(see below); the display's demands are a rendering profile, deferred.
### Wall-panel
- CHORE/UI: identical in-flight chart requests are deduplicated per browser tab,
so two wall panels showing the same tile still run the query twice. An
@@ -348,12 +298,7 @@ Decisions taken up front, because most items below depend on them:
answered by flow inputs. What is left is the naming: an input a panel writes
looks the same as one a run passes in.
Deliberately not ported: the local-state/timestamp reconciliation the old
dashboard does per widget — publishing on release and reading the value back
covers it — and its demo mode, since an unbound or silent message already renders
as an em dash.
### Dashboard follow-ups
### Dashboard
- FEAT/NODE: the hosted demo places six of the fifteen built-in node types (`python`, `inject`, `change`, `join`, `rbe`, `trigger`); it does cover all fifteen dashboard widget types. `switch` and `delay` are the awkward ones — a `switch` branch needs either a dead-end port or trivial nodes to turn a branch back into a label, and neither reads as something a person would hang — while the I/O types (`mqtt`, `http`, `influxdb`, `exec`, `file`, `ntfy`, `mlp`) are unplaced because the demo has nothing real to talk to. Worth revisiting when the demo grows a second page.
- CHORE/UI: multi-page and multi-section dashboards have no UI and need none — a panel carries