Add the dashboard settings channel, wired for theme and lock

A dashboard could only ever receive as a set of tiles. This adds the dashboard
itself as a receiver: `settings` maps a name to a value plus an optional
binding. Unbound, the setting is simply its value — a wall panel that is always
dark costs no flow. Bound, a flow drives it live and the value is the fallback.

Two settings are wired: `theme` (system/light/dark) and `locked` (read-only).
There is no schedule field on purpose — a node publishing to the bound message
on a cron is what a schedule is here, which is the point of a channel.

- `messages_for()` now walks a dashboard's bound settings as well as its
  widgets' bindings. Without this a paired screen is refused its own theme
  message, on the one surface the setting exists for; it bounds the socket too.
- `locked` is gated in `usePublish`, so every control inherits it, and each
  control also draws itself disabled — a dead button reads as broken otherwise.
  The panel surface says Read-only in the corner.
- The theme is a class on the dashboard's own surface, never the root: inside
  the app shell it must not flip the chrome. `.light` gains the tokens `.dark`
  already had (mirrored in the index repo) so both directions work on a subtree.
- Settings bindings are type-checked from the document alone, the rule widget
  bindings follow, and mirrored on the server.
- A bound setting is drawn on the flow canvas as a dashboard-level endpoint.
- The demo's house flow now publishes `home.panel_theme`, which the demo
  dashboard's theme binds to: the panel goes dark after sunset, at no tile cost.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tULRZJUkZsw7rMJ3h4xvu
This commit is contained in:
2026-08-22 13:17:56 +02:00
co-authored by Claude Opus 5
parent 3e7b161950
commit d958d7cde6
18 changed files with 784 additions and 62 deletions
+44 -3
View File
@@ -86,6 +86,44 @@ it, so swapping the store is a change to one flow and nothing else. The answer
also states what it was computed for, so an answer to a different question is
ignored rather than two charts overwriting each other's picture.
## Dashboard settings
Most of what a dashboard carries is a widget: a tile bound to a message. Two
things are not, because they belong to the whole surface rather than to any
tile on it — and a screen bolted to a wall has nobody standing at it to set
them.
| Setting | Is | Driven by |
|---|---|---|
| **Theme** | `System`, `Light` or `Dark` | a `str` message |
| **Lock** | read-only on or off | a `bool` message |
Both work the same way, and both halves are optional:
- **Just a value.** Set Theme to `Dark` and that dashboard is dark wherever it
is shown, whatever the device or the browser prefers. This costs no flow at
all, and it is what most wall panels want.
- **Driven by a message.** Pick one in **Driven by** and a flow takes the
setting over, exactly as it drives a tile. The value you set stays the
fallback: what the dashboard uses before the first message arrives, and
whenever the message is silent.
A bound setting is type-checked like a widget binding — a Theme pointed at a
`float` is refused by the editor and by the server — and the panel's credential
is extended to it, so a paired screen may read its own theme message and
nothing further.
There is no schedule field, on purpose. **A schedule is a node publishing to
the bound message**: an `inject` with a cron expression, feeding a `change`
node that maps the hour onto `"dark"` or `"light"`, is the whole of "dark after
sunset" — and the same channel then serves anything else you want to drive,
including locking a panel down remotely.
Lock is a read-only *surface*, not a permission. The controls stay visible,
stop publishing and read as disabled, and the panel says **Read-only** in the
corner. What a paired screen is allowed to reach is still decided by its own
credential, below.
## Showing one
- `/view/{name}` — a browser tab pointed at one dashboard. Needs an ordinary
@@ -110,9 +148,12 @@ What the screen holds is not a login. It reaches that panel's published
dashboards and the messages its own widgets read or publish, and nothing else —
a message no tile on it draws is refused in both directions.
It cannot be made strictly read-only, and that is honest rather than an
oversight: a querying chart publishes its request, and a control on a panel is
the reason you put one there.
The *credential* cannot be made strictly read-only, and that is honest rather
than an oversight: a querying chart publishes its request, and a control on a
panel is the reason you put one there. The dashboard's own **Lock** setting
above stops its controls publishing and can be driven by a flow, which is how
you quieten a screen remotely — but that is the surface behaving, not the
credential being narrowed.
Deleting the panel revokes the credential and the assignment together, which is
how you retire a device and what it showed. Unpairing revokes only the