docs: record the structured-dtype and querying-chart decisions

Also the one the session settled by hand: database nodes stay transport
and credentials, and the Python nodes either side do the query building
and the answer shaping. That is what a series read mode inside the node
would have prevented, so the read-mode item goes with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 15:15:28 +02:00
co-authored by Claude Opus 5
parent 19c196b7e6
commit 61bfd68f26
2 changed files with 36 additions and 33 deletions
+22 -33
View File
@@ -50,7 +50,6 @@ is what M4 still waits on, together with porting the flows.
- 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/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. - 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.
- CHORE/FLOW: `_to_messages` keeps its `if not retval: return None` guard ahead of the new type check, so a falsy non-dict return (`0`, `""`, `[]`) is still silently "no output" rather than the named error. Deliberate for now; worth a decision.
- CHORE/FLOW: `WorkItem.kind == "node"` ("executes exactly one node") was documented but never implemented. If a run-one-node item is wanted, it still needs writing. - CHORE/FLOW: `WorkItem.kind == "node"` ("executes exactly one node") was documented but never implemented. If a run-one-node item is wanted, it still needs writing.
- CHORE/API: `GET /observability/runs` caps at 200 rows, so pinning a minute busier than that on the Home chart shows its newest 200 with nothing saying more exist. Fine at ~60 runs/min; a count alongside the rows, or paging, is what a busier instance needs. - CHORE/API: `GET /observability/runs` caps at 200 rows, so pinning a minute busier than that on the Home chart shows its newest 200 with nothing saying more exist. Fine at ~60 runs/min; a count alongside the rows, or paging, is what a busier instance needs.
@@ -97,28 +96,27 @@ Decisions taken up front, because most items below depend on them:
- A chart asks a flow for its series the way every other input widget speaks: - 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 it publishes a request message and reads the answer. No query API, no
database knowledge in the widget. 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 - Nothing e-ink-specific in the widgets. Panel access is a credential problem
(see below); the display's demands are a rendering profile, deferred. (see below); the display's demands are a rendering profile, deferred.
- !FEAT/FLOW: declared structured dtypes — `series` (labelled ts/value pairs), - FEAT/UI: assemble the range-aware demo dashboard now that charts can query —
`record` (flat named scalars) and `list` (ordered items of a declared shape) in an InfluxDB node behind a build/parse pair, with the panel's own range picker
`DType`, checked like the scalars are, with `WIDGET_DTYPES` extended so a governing the window. The pieces are in and verified against a real bucket;
widget binds a shape rather than "some JSON". Everything a single number can what is missing is a dashboard someone would actually hang.
say stays a `float`/`bool` message. Prerequisite for the agenda, forecast, - CHORE/UI: identical in-flight chart requests are deduplicated per browser tab,
notification and queried-chart widgets. so two wall panels showing the same tile still run the query twice. An
- !FEAT/UI+FLOW: charts that query. A chart owns a refresh interval, publishes a `interval` on the request port is the backstop, and it belongs to the flow
request message (range, interval) exactly as a slider publishes a value, and serving the request rather than to the widget asking.
draws the `series` message a data node answers with. The answer carries the - CHORE/FLOW: one request/answer pair per InfluxDB node — the first input
range and interval it was computed for and the widget ignores one that does not carrying a `flux` key is the request and the answer leaves on the first output
match its request — otherwise two charts, or two panels, on the same node port. A second query stream through one node needs a second node.
overwrite each other's window. Costs a duplicate query rather than a wrong
chart. Needs a floor under the interval and dedup of identical in-flight
requests. The alternative — a message name per widget — would mean dynamic
names in a namespace that is deliberately static. Overall idea is that visualization and data stays separate. The widget can make a query to an influx db node (with some python function in front of it to prepare the query) and the node then feeds the answer back to the widget (again with some python function to parse the response). This makes the widget agnostic of any database type
- FEAT/NODE: a series read mode for the InfluxDB node: return the whole
aggregated window (`aggregateWindow`, function and interval from the request)
as one `series` message, instead of the single aggregated scalar it produces
now. This is what the environment, power and server pages are made of.
- FEAT/UI: bar/level widget — a horizontal bar with its value written on it, and - FEAT/UI: bar/level widget — a horizontal bar with its value written on it, and
a nested bar for a quantity contained in another (PV inside inverter input). a nested bar for a quantity contained in another (PV inside inverter input).
The arc gauge is the only level display today. The arc gauge is the only level display today.
@@ -129,20 +127,13 @@ Decisions taken up front, because most items below depend on them:
- FEAT/UI: icon-by-value widget — a mapping from value or range to a lucide icon - FEAT/UI: icon-by-value widget — a mapping from value or range to a lucide icon
and a colour token. Carries both the weather condition icon and derived hints and a colour token. Carries both the weather condition icon and derived hints
such as "indoor dewpoint above outdoor, open a window". such as "indoor dewpoint above outdoor, open a window".
- FEAT/UI: agenda widget over a `list` message — the next N items with relative
day labels ("Today", "Tomorrow", weekday, then date) and truncated titles.
- FEAT/UI: forecast strip widget over a `list` message — N columns of label, - FEAT/UI: forecast strip widget over a `list` message — N columns of label,
icon and value, with the later columns drawn progressively dimmer. icon and value, with the later columns drawn progressively dimmer.
- FEAT/UI: notification widget — title and body from a `record` message, held
until replaced. Worth wiring to the alerting events rather than a message of
its own.
- FEAT/UI: clock widget — local time and date, bound to nothing. - FEAT/UI: clock widget — local time and date, bound to nothing.
- FEAT/UI: chart presentation — axis titles, a unit, an optional fixed y range, - FEAT/UI: chart axis titles. The unit, the fixed y range and the per-series
and the per-series label the document already stores but the editor never label are in; a named axis is not.
offers. - FEAT/UI: the slider offers `step` now, but no tick labels — the `datalist`
- FEAT/UI: the slider panel exposes only min and max; the renderer already reads marks are unlabelled and drop out past fifty steps.
`step` and `unit`. Add those plus tick marks (a 2022 °C setpoint at 0.1 is
unusable without them).
- FEAT/API: a kiosk credential for `/view/{name}`, so a panel is not a - FEAT/API: a kiosk credential for `/view/{name}`, so a panel is not a
logged-in browser session. Note it cannot be strictly read-only: a querying logged-in browser session. Note it cannot be strictly read-only: a querying
chart publishes its request, so the token needs that one write scope. chart publishes its request, so the token needs that one write scope.
@@ -172,7 +163,6 @@ as an em dash.
- FEAT/UI: only `layout.lg` is ever written. Below `lg` the view stacks widgets full width in CSS, so `md`/`sm` stay unused until a per-breakpoint editor exists. - FEAT/UI: only `layout.lg` is ever written. Below `lg` the view stacks widgets full width in CSS, so `md`/`sm` stay unused until a per-breakpoint editor exists.
- PERF/UI: `ChartWidget` re-joins the whole table on every live value. Fine at IoT rates; at `HISTORY_CAP` × 5 series it should append into a ring buffer. - PERF/UI: `ChartWidget` re-joins the whole table on every live value. Fine at IoT rates; at `HISTORY_CAP` × 5 series it should append into a ring buffer.
- CHORE/UI: opening edit mode on a dashboard whose widgets predate placement writes the migrated positions immediately, bumping the version once. - CHORE/UI: opening edit mode on a dashboard whose widgets predate placement writes the migrated positions immediately, bumping the version once.
- CHORE/API: no backend test for the `WidgetDef` dtype validator or `columns`.
- FEAT/UI: `POST /dashboards/{name}/discard` has no button. The flow settings panel offers "discard draft"; the dashboard settings panel does not, so an unwanted edit can only be undone by hand or by publishing it. - FEAT/UI: `POST /dashboards/{name}/discard` has no button. The flow settings panel offers "discard draft"; the dashboard settings panel does not, so an unwanted edit can only be undone by hand or by publishing it.
- CHORE/API: creating a dashboard publishes it straight away (an empty document goes to the panels), while a new flow starts as a draft. Keeps `read`/`list` free of a never-published case, at the cost of the asymmetry. - CHORE/API: creating a dashboard publishes it straight away (an empty document goes to the panels), while a new flow starts as a draft. Keeps `read`/`list` free of a never-published case, at the cost of the asymmetry.
- PERF/UI: "Publish all" reads each document's detail for the version its publish must match, so a click is 2N requests. A bulk endpoint, or a `version` on the summaries, would make it one. - PERF/UI: "Publish all" reads each document's detail for the version its publish must match, so a click is 2N requests. A bulk endpoint, or a `version` on the summaries, would make it one.
@@ -217,7 +207,6 @@ Open on purpose. Each names what should bring it back.
- CHORE/INFRA: `bun run --filter frontend build` fails on this workspace with `crypto.hash is not a function` — Vite 7 wants Node 20.12+ and the host has 18. The Docker image builds fine, so it only bites local bundling; `bunx tsc` still type-checks. - CHORE/INFRA: `bun run --filter frontend build` fails on this workspace with `crypto.hash is not a function` — Vite 7 wants Node 20.12+ and the host has 18. The Docker image builds fine, so it only bites local bundling; `bunx tsc` still type-checks.
- FEAT/UI: an endpoint's edge routes straight across the graph, so it can pass behind a node that sits between the lane and the node it wires to. Readable, but a routed edge would be tidier. - FEAT/UI: an endpoint's edge routes straight across the graph, so it can pass behind a node that sits between the lane and the node it wires to. Readable, but a routed edge would be tidier.
- FEAT/UI: the node-panel and edge trend curves take no range, unlike the health block. They are drawn from a Redis ring of the last 120 values per message, which has no window to ask for — a hover caption names what the curve covers instead of a picker promising a span nothing can serve. Reopen if per-message history ever gains a time window. - FEAT/UI: the node-panel and edge trend curves take no range, unlike the health block. They are drawn from a Redis ring of the last 120 values per message, which has no window to ask for — a hover caption names what the curve covers instead of a picker promising a span nothing can serve. Reopen if per-message history ever gains a time window.
- FEAT/UI: a dashboard that demonstrates the time range concept, with an InfluxDB node as its data source. The range picker governs the health block, which is backed by minute rollups; a dashboard chart draws a message's 120-sample ring, and the InfluxDB node answers a query as one aggregated scalar over a `query_range` fixed in its params. So a range-aware dashboard chart is the *charts that query* item under wall-panel parity — the `series` dtype, the chart publishing its request, and an InfluxDB series read mode — not a dashboard anyone can assemble today. Reopen with that work.
- FEAT/UI: an e-ink rendering profile for a dashboard — motion off, hover-only affordances resolved to something visible, high-contrast palette, thick strokes, and a repaint cadence low enough for a display that takes a second to settle. Reopen when a panel with such a display is actually hung. - FEAT/UI: an e-ink rendering profile for a dashboard — motion off, hover-only affordances resolved to something visible, high-contrast palette, thick strokes, and a repaint cadence low enough for a display that takes a second to settle. Reopen when a panel with such a display is actually hung.
- CHORE/INFRA: Postgres stays. The 2026-08 review rejected YugabyteDB/CockroachDB (multi-node cluster systems, ~4 GB+ RAM per node, against the small-server target — the scaling story is remote workers, not a distributed DB) and found merging Postgres into Redis or vice versa buys little: the stores hold disjoint data and both sit behind abstractions. SQLite would fit the single-instance design and drop a container; reopen if the home-install footprint becomes a product concern. - CHORE/INFRA: Postgres stays. The 2026-08 review rejected YugabyteDB/CockroachDB (multi-node cluster systems, ~4 GB+ RAM per node, against the small-server target — the scaling story is remote workers, not a distributed DB) and found merging Postgres into Redis or vice versa buys little: the stores hold disjoint data and both sit behind abstractions. SQLite would fit the single-instance design and drop a container; reopen if the home-install footprint becomes a product concern.
- CHORE/INFRA: NATS JetStream as the work-queue backend — durable streams whose consumer semantics match the `WorkQueue` interface, in one small binary. Reopen with M5 remote workers, when the queue crosses hosts. - CHORE/INFRA: NATS JetStream as the work-queue backend — durable streams whose consumer semantics match the `WorkQueue` interface, in one small binary. Reopen with M5 remote workers, when the queue crosses hosts.
+14
View File
@@ -216,6 +216,20 @@ Shares components with the admin view. See `docs/architecture/structure.canvas`
- [x] Draft and publish, as flows have it: the editor autosaves - [x] Draft and publish, as flows have it: the editor autosaves
`dashboard.draft.json` and a panel reads only the published document, so a `dashboard.draft.json` and a panel reads only the published document, so a
half-arranged page never reaches the wall until someone publishes it half-arranged page never reaches the wall until someone publishes it
- [x] Declared structured payloads — `series`, `record` and `list` (with its item
type) join the scalars in `DType` and are checked the same way, so a widget
binds a shape rather than "some JSON" and a wrong binding is refused before
anything runs. An agenda over a `list` and a notification over a `record`
are the first two to read them, the latter fed either by a flow or by the
`dashboard` alert channel
- [x] Charts that query: a chart publishes a request — the window and the
resolution — exactly as a slider publishes a value, and draws the `series`
a flow answers with. The answer says what it was computed for and one
computed for another window is ignored, so two charts on a node cost a
duplicate query rather than the wrong picture. Database nodes stay
transport and credentials only: the InfluxDB node runs Flux handed to it
and echoes the rest, and Python nodes either side build the query and shape
the answer — which is what keeps the widget ignorant of the database
- [ ] Per-device view - [ ] Per-device view
## Phase 5 — Website and docs ## Phase 5 — Website and docs