From 9bdced74fb80ebd92251e9d7e4a052d96cb07db8 Mon Sep 17 00:00:00 2001 From: stroblme Date: Fri, 21 Aug 2026 14:35:24 +0200 Subject: [PATCH] NOTEPAD: drop what wave two closed, file what it turned up Twenty-three items are done: the dashboard draft/publish split and panel freshness, the canvas clipping warning, the inject node's per-port payloads, the sticky failure and the brain click-through, and the lint and DOMAIN tooling fixes. The ChartWidget entry is rewritten rather than closed. Its ring buffer would only remove the cheap half of the cost, and it is the shape most likely to tempt someone into re-adding the dependency array that made a full window of chart data go stale. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Uq8mtNb97A7praJLyeEYgs --- NOTEPAD.md | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/NOTEPAD.md b/NOTEPAD.md index c6fcab3..e3101c7 100644 --- a/NOTEPAD.md +++ b/NOTEPAD.md @@ -12,6 +12,28 @@ Deferring because out of scope is fine, but don't mention deferring than. ### To be sorted +- BUG/UI the "Installation Offline" warning (and notification in general) should be centered w.r.t. the viewport (currently it is a bit left, discarding the width of the sidebar). Furthermore, the notification does not seem to disappear on its own. Reloading the page solves it +- CHORE/INFRA: the `generate-frontend-sdk` pre-commit hook runs `scripts/generate-client.sh` + on any `backend/**` change, and that script ends by formatting the whole frontend tree — + while `biome.json` excludes `src/client`, so it formats nothing the generator wrote. Every + backend commit therefore rewrites files it never touched. Dropping the trailing format call + is the fix; it was kept this wave only to preserve behaviour. +- CHORE/UI: acknowledging a node's failure clears it on the engine but publishes no event, so + another browser watching the same flow keeps the marker until its next snapshot or rebuild. + One event on the bus would close it, the way `dashboard_changed` does for panels. +- CHORE/API: `save_dashboard` still catches `DashboardNotFound` from `write_draft`, which can + no longer raise it. Harmless, and the same shape `saveFlow` has: a PUT to an unknown name now + creates that dashboard's first draft rather than answering 404. +- CHORE/UI: the editor-side `ModePicker` and `StylePicker` in `panels.tsx` are still the older + flex row with a jumping `bg-accent` fill, while the widget-side segmented control and + `RangePicker` now slide one thumb over equal grid tracks. Two shapes for one control. +- CHORE/UI: an icon set on a dashboard cannot be cleared from the UI — Radix forbids an empty + `SelectItem` value, so neither the rail-icon select nor the existing "Otherwise" select + offers a "none". Both would need the same affordance. +- FEAT/UX add an option to the settings of an installation to configure automatic updates. If enabled, the installation would send a request e.g. every 1h to the hub at fluksio.com and the hub then checks if a new version is available. The settings should include a second toggle for automatically installing an update (which might cause a short outage). Later this mechanism should be extended to check if updating would cause things to break. +- FEAT/UI allow setting icons for multi-page dashboard (when configuring a panel, we could simply add an icon picker there) +- BUG/UI hovering the sidebar where we can switch dashboards on a multi-page dashboard shows a horizontal scrollbar. We should remove that; no scrollbars at all should be shown in this type of sidebar +- BUG/UI the "Installation Offline" warning (and notification in general) should be centered w.r.t. the viewport (currently it is a bit left, discarding the width of the sidebar). Furthermore, the notification does not seem to disappear on its own. Reloading the page solves it - CHORE/UI: `.u-legend` is styled in the dashboard's own CSS chunk, so a uPlot legend on a Health or Home page renders unstyled until a dashboard has been visited in that session. `RangePicker` already side-effect-imports `dashboard.css` for the segmented thumb; the @@ -31,10 +53,8 @@ Deferring because out of scope is fine, but don't mention deferring than. - CHORE/API: `DELETE /flows/{name}` does not refuse while the flow has a `running` or `queued` Run. `RunService._record_node` can then insert `run_node` rows for a run that no longer exists; `_finish` is an UPDATE, so it degrades to a harmless 0-row no-op. -- CHORE/INFRA: `backend/htmlcov` can be left root-owned by a container run, and `make test-backend` then fails at the coverage HTML step *after* every test has passed — which reads like a test failure and is not one. -- CHORE/INFRA: `.gitea/workflows/playwright.yml` says it shards "Two spec files (tests/flows.spec.ts, tests/admin.spec.ts)". There are nine. - FEAT/UI add (multi-)select to the flows and dashboards view to allow deleting (multiple) items; long press to select -> "Add" button should change into "Trash" icon button - BUG/UI in the brain view: make the chasing circle animation running entirely in the gap between the ring and the node (using the full width) @@ -46,7 +66,6 @@ Deferring because out of scope is fine, but don't mention deferring than. - BUG/UI on flows like "House history" where the widget sets the range for the "draw the window" node to generate some data, the edges overlap the nodes. We should adjust the flow visualization to account for these cyclic behaviors - CHORE/UI: loop lag on Home reads a real number with no flows, and that is right — `LoopWatchdog` times how late `asyncio.sleep(1.0)` wakes on the API's event loop and is started unconditionally, so it measures the engine process rather than any flow, and it is what turns the health badge `degraded`. Nothing to fix; recorded so it is not reopened. - BUG/UI auto node placement on flows should be improved in regards to least crossing edges and a more vertical layout on mobile devices -- FEAT/UI we should highlight failing nodes accordingly in the flow view to facilitate easier tracking of mis-configurations - INFRA: ensure that all the packages/ dependencies needed to run fluksio are available on arm to make this software runnable on e.g. raspbian - INFRA: merge the philosophy statement at the beginning of vision.md into the rest of the document. Dissolve the decision dates and fold the decisions into a clean structure - FEAT/UI add a loading animation for the initial app load and when loading individual pages; make sure that elements e.g. in the home dashboard load independently to ensure a fast loading of the initial site but figures charts, tables, graph etc. follow after that @@ -183,50 +202,32 @@ as an em dash. ### Dashboard follow-ups -- BUG/UI: ensure dashboard wallpanel (read-only) links hot reload automatically on dashboard changes - 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: an icon rule stores a `label` — the widget draws it under the glyph and uses it as the glyph's accessible name — but the mapping editor offers no field for it, so it can only be set through the API. -- BUG/UI: shrinking the canvas silently clips whatever now falls past its bottom edge. `maxRows` only constrains a new drag, not a stored placement, so nothing warns and nothing offers to reflow. -- CHORE/UX: dropping a widget also selects it, which opens its panel — which rescales the canvas the instant you let go. Correct, but it lurches; either leave the panel closed on a drag-release or animate the scale. - CHORE/UI: `ROW_HEIGHT` is a fixed 80px while column width follows the canvas, so a 1920-wide panel at 12 columns has 160×80 cells. If that reads too wide, the row height could derive from the canvas too. - CHORE/UI: multi-page and multi-section dashboards still have no UI, and now need none — a panel carries several whole dashboards instead, each with its own canvas and its own publish. `PageDef`/`SectionDef` stay in the schema and the editor still edits `sectionsOf(page)[0]`, so the page `Tabs` in `DashboardEditor` are dead until something writes a second page through the API. -- FEAT/UI: a panel does not notice being reassigned until it is reloaded — nothing pushes the panel document or a dashboard publish, so the rail is as stale as the last read. Same gap as the wallpanel hot-reload item above; one event on the bus would answer both. - CHORE/API: a panel credential may publish *any* message, not only the ones its own widgets bind to — the allowlist is the `/messages/` prefix rather than a walk of the panel's widgets. The walk now exists: `panels.messages_for()` is what bounds the live socket. Pointing `_panel_may` at it would close this too, but it tightens what already-paired screens may do, so it wants a deliberate look at the query-chart request path first. - CHORE/API: unpairing a device means deleting the panel. A per-panel nonce in the token, bumped on demand, would let one screen be re-paired without disturbing the assignment. - CHORE/API: a panel paired through the portal is revoked here the moment the panel is deleted — `_panel_may` finds nothing and answers 401 — but the hub's copy of the token stays valid until it expires or the installation's generation counter is bumped ("New code"). The hub has no per-panel revocation, and giving it one means telling it which panels exist, which is exactly what this design avoids. The generation bump is the lever; it is blunt, cutting every credential the portal minted for the installation. - CHORE/UI: the device line under a pairing code is the raw user agent plus the address the request came from. Both are self-reported and neither is proof; it is there so an admin can tell the screen they just hung from one they were not expecting, not to authenticate anything. -- BUG/UI: the panels dialog is open to any signed-in user, but assigning dashboards is a superuser's (`PUT /panels/`). A non-superuser ticking a checkbox gets a 403, which `main.tsx` treats as a dead session and logs them out. The pairing form is gated on `is_superuser` now; the assignment checkboxes are not. - CHORE/API: `POST /panels/pair` is reachable from the internet once an installation is enrolled — the hub forwards it without a session, since a device with no credential is the point of it. Bounded three ways (the hub's per-installation and per-address limits, and the fifty-code cap here), but it is the first unauthenticated surface this installation exposes outward. - CHORE/API: `POST /panels/pair` is unauthenticated and capped at fifty pending codes in one process. A second API worker would each keep their own dictionary, so pairing would work only when the poll lands on the process that minted the code. The same holds for a screen pairing through the portal, which lands on whichever worker holds the tunnel. -- CHORE/UI: the rail draws two letters off the dashboard title. `PageDef` already stores a lucide icon name; a dashboard-level one would read better on a wall. - CHORE/UI: only `layout.lg` is ever written, and `md`/`sm` stay unwritten by decision — a phone stacks the widgets (`.widget-stacked`) rather than carrying an arrangement of its own, since arranging is not a phone feature. The keys stay in the schema for a panel that one day wants a second size. -- 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`'s cost per live value is the `uPlot.join` in `UplotChart`, not the tail append — the fetched half comes from React Query and is replaced wholesale on every refetch, so a ring buffer over the live tail would leave the dominant cost untouched. If this is ever profiled and fixed, the `setData` effect must stay dependency-free: a mutable buffer's identity never changes, so keying the effect on it reintroduces the staleness that the point-count dependency used to cause, and more quietly. - CHORE/UI: opening edit mode on a dashboard whose widgets predate placement writes the migrated positions immediately, bumping the version once. -- 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. NOTE: we should change this such that dashboards stay drafts until they get published -- 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. ### Flow editor follow-ups -- BUG/UI: the payload of the inject node should adapt to the type set in the "provides" arguments and the number of fields should grow with the number of "provides" arguments. So if e.g. one argument is boolean, the payload should be a dropdown for true/false. If it is int, the value entered should be parsed as int etc. `payload` is typed `Any`, so `ParamsForm` renders one plain text field and never sees the node's `provides` at all. The dtype→widget pieces to reuse are `parseByDtype` and the bool dropdown in `FlowBoundary.tsx`, written for flow inputs and the run dialog. -- CHORE/UI: a node's error status clears as soon as it runs again, so a failure that genuinely fired an alert can leave no trace on the canvas by the time anyone looks. The logs panel keeps the traceback; the node itself reads as healthy. - PERF/FLOW: every save rebuilds the whole pipeline. Fine at the current flow count; rebuild only the touched flow when it starts to show. - CHORE/API: `POST /flows/{name}/rename` is no longer reachable from the UI. A flow's title is what the panel edits, matching how nodes work; the canonical name is fixed at creation, so either the endpoint goes or renaming comes back deliberately. - CHORE/UI: ⌘C/⌘V `preventDefault` on the canvas blocks the native clipboard there (fields are guarded). The node clipboard is `localStorage`, so it does not cross browsers or profiles. - PERF/UI: `useParamSuggestions` fetches every flow's detail to build the suggestion list. An aggregate endpoint if an installation ever has many flows. -- CHORE/UX: the derived-cron chip also appears on the delay node, where `interval` is a rate limit rather than a schedule. May want it inject-only. - CHORE/UX: free-form params (python nodes) get no suggestions, since there is no schema to key them off. - PERF/UI: `BrainView` runs 300 force-layout ticks synchronously inside a `useMemo`, so the graph is laid out on the render thread. - FEAT/UI: the brain is a band on a scrolling page now, so it neither pans nor zooms — the fit keeps the whole graph in view instead. An installation with enough flows to make the labels unreadable at that fit needs a way to open the graph larger. - CHORE/UI: React Flow measures a node's handle bounds out of the DOM once and never again, and in the brain that one measurement falls inside the graph's `scaleIn` entrance — so every `sourceX`/`targetX` it hands an edge there is the entrance's 4% short of the centre, permanently. `BrainEdge` takes both ends from the layout instead (position + radius). Any future view that mounts a canvas inside a transform and reads node internals meets the same thing. -- CHORE/UI: a value passing lights the brain's connection blue for the pulse, but its two end dots only follow the slower falloff, so the flash stops where the line does. -- CHORE/UI: `flow.css` drops the focus outline on every `.react-flow__node`. Brain neurons now answer `:focus-visible` with the same border colour as hover, but the flow editor's own nodes still have no visible keyboard focus. ### Infrastructure -- CHORE/INFRA: the `playwright` compose service cannot reach `api.localhost` and is not in the `dev-local` set, so `make verify-docker` is the only containerised route. Running natively is no longer dependable either: the workspace pins browser revision 1208 and `~/.cache/ms-playwright/chromium-1208` has no binary, so a host run either fails or silently borrows the 1234 build a `bunx playwright@latest` left behind. `bunx playwright install` against the pinned version is what would settle it. (The emoji font is installed now — 👋 no longer renders as tofu.) -- CHORE/UI: `make lint-frontend` is `biome check --write --unsafe ./` — a lint target that rewrites the whole tree rather than checking it. A checking target plus a separate `format` would be safer. -- CHORE/UI: the alerts screen duplicates the backend's `ALERTING_EVENTS`; the chooser drifts if the backend set grows. A rule with nothing ticked covers everything, so it fails soft. -- CHORE/UI: `tests/runtime.spec.ts` still calls the home page "the dashboard" (`dashboard-flow-row`), which now collides with the dashboards feature. - CHORE/INFRA: `make soak`'s redis scenario stops the container the whole stack shares, so every flow briefly fails to journal, not just the soak fixtures. They recover on their own — nothing was dead-lettered or quarantined in the run this note comes from — but it is not a thing to run against a stack someone is relying on. - CHORE/INFRA: the soak harness's engine kill only catches a couple of items unacknowledged, because a cascade finishes in about four milliseconds. Redelivery is proven but barely stressed; a fixture node with a deliberate sleep would widen the window enough to test it properly.