Both screens the house is looked at on are 1280x800, so that is what the three dashboards are laid out for: twelve columns of 96px, twelve rows of 51px, and nothing past the bottom, because a panel does not scroll. The motors are one control each instead of three buttons. A button could only publish; a segmented control reads back as well — so the motor writes what it is doing to the same message the control sets, and the segment that is held is the direction it actually went. Up, Stop, Down for the shutters; Close/Open for the window and In/Out for the awning, which is what those two are for. A run stopped part way now leaves the position unknown rather than claiming the target it never reached, so the next command in either direction moves it. The preflight gained the two checks this needed. One runs each sample shape past the port that would receive it. The other is arithmetic: every tile inside the panel and none on top of another — both silent failures on a screen with no scrollbar, and both caught before anything is written. Sizes were settled by looking. A slider needs three rows or its tick labels fall off; a status icon needs three or it loses the word under the glyph; a gauge in two rows has no arc worth reading, so the battery is a bar on Home and a gauge on Energy where there is height for one. A chart spends eighty pixels on its chrome whatever it is given, so two of them read on this panel and three did not — the temperature history is the one that went, and `history` still answers for it. `capture-panels.mjs` is how that was checked: the three panels at the screen's own pixels, in both themes, reporting whether anything spilled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
409 lines
44 KiB
Markdown
409 lines
44 KiB
Markdown
This file captures tasks which derive from roadmap tasks (unfinished, deferred), bugs encountered during usage and feature requests/improvements which are not fitting directly in the roadmap.
|
||
Always sort by priority and put tasks blocked by other tasks/features at the dedicated section.
|
||
When working on a task, check for other, similar tasks that could be resolved on the way.
|
||
Use following pattern to classify tasks: TYPE/SCOPE
|
||
Where TYPE could be BUG, FEAT, PERF, CHORE and SCOPE could be UX, UI, FLOW, NODE, API, INFRA, DOCS appended by MOBILE if only for mobile use case.
|
||
Don't write temporary reasons for deferring a task in the task description (only strategical reasons should be noted).
|
||
Deferring because out of scope is fine, but don't mention deferring than.
|
||
|
||
`Deferred` holds what stays open on purpose, each with the condition that should reopen it.
|
||
|
||
## Open
|
||
|
||
### To be sorted
|
||
|
||
- CHORE/UI: the house panels are laid out for 1280x800 — twelve columns, twelve
|
||
rows — and that is as much as fits: a chart spends about eighty pixels on its
|
||
title, range picker and legend whatever height it is given, so two of them
|
||
read on that panel and three do not. The temperature history was the one
|
||
dropped; `history.climate_*` still answers, so it is a tile away.
|
||
- FEAT/UI: a bar's nested reading is captioned by its port name, which is
|
||
chosen for the graph rather than for somebody reading it across a room.
|
||
`Segment` now takes an optional `label`; the house dashboard sets one
|
||
(`Solar`), and it shows on the next frontend build.
|
||
- BUG/INFRA: the running `fluksio-frontend` image calls `https://api.localhost`
|
||
while `app/.env` says `VITE_API_URL=http://api.localhost` — the image predates
|
||
the environment being switched to `local`. Anything served from that bundle
|
||
reaches the API only where something terminates TLS for `api.localhost`. The
|
||
next frontend build changes the origin, so check how the app is actually
|
||
reached before making one.
|
||
|
||
- BUG/FLOW: **a cancelled request can leave `FlowController._lock` held forever.**
|
||
Seeding nineteen flows over a client that timed out mid-request left the next
|
||
`POST /flows/{name}/start` waiting on the lock indefinitely — ten minutes, until
|
||
the container was restarted. A py-spy dump showed *no* thread in the reload path,
|
||
so the coroutine that holds it is suspended at an `await` inside `reload()`, most
|
||
likely in `_teardown()` awaiting a supervised task's cancellation. Everything else
|
||
kept working — health, reads, MQTT — so the engine looked fine and only anything
|
||
needing a rebuild hung. Two things worth doing: release the lock on cancellation
|
||
(`asyncio.timeout` around the teardown, or a `finally` that cannot be skipped), and
|
||
fail a `start` that waits more than a few seconds for the lock rather than hanging.
|
||
- BUG/INFRA: **475 zombie `git` processes** in the API container after a seeding
|
||
session. `FlowStore._git` uses `subprocess.run`, which reaps its own child — these
|
||
are the `git gc --auto` daemons `git commit` spawns, reparented to PID 1 when their
|
||
parent exits. PID 1 is the FastAPI process, which never reaps orphans. Harmless at
|
||
484 processes against a 34719 limit, but it grows with every save, and a container
|
||
that commits per keystroke will get there. Fix: `git -c gc.auto=0 commit`, since a
|
||
store that never packs is a separate (and real) concern — 1898 commits had left
|
||
5863 loose objects and no packs.
|
||
- PERF/API: seeding nineteen flows takes two reloads each — one to publish, one to
|
||
stop — so a full rebuild of every flow in the installation runs about forty times
|
||
for one seed. It is the slowest thing about standing an installation up, and a
|
||
`PUT` that could say "published, stopped" in one call would halve it.
|
||
|
||
- FEAT/SEC: `locked` is a read-only surface, not a permission — the server accepts a publish
|
||
from a panel whose dashboard says locked. Making it real means carrying the flag into
|
||
`_panel_may`.
|
||
- FEAT/UI: a dashboard forced light inside a dark app shell gets correct tokens but still
|
||
matches `dark:` variant utilities, because those compile to `&:is(.dark *)` and `<html>`
|
||
always carries a theme class. Cosmetic (a few faint shadcn backgrounds); fixing it properly
|
||
needs a theme-scope mechanism CSS ancestor selectors cannot express today.
|
||
- CHORE/UI: the lock notice is on `PanelSurface` only, so `/dashboards/{name}` in read mode
|
||
shows disabled controls without the pill.
|
||
- CHORE/DOC: the docs call a cron `inject` node "a node publishing on a schedule". The node
|
||
type literally named `trigger` is a debounce/hold node, not a scheduler — worth renaming one
|
||
of the two eventually.
|
||
- CHORE/DEMO: the demo panel now fills all 17 rows its 2560×1600 canvas holds, every row
|
||
across all 16 columns. Another tile needs a rearrangement first — worth doing together
|
||
with re-authoring `PAGES` as a single section.
|
||
- CHORE/TEST: no Playwright coverage for the colour tile — `tests/widgets.spec.ts` covers the
|
||
other controls. Wants a spec that drags the hue ring and asserts one publish on release.
|
||
- CHORE/UI: the colour wheel's arrow keys move a fixed 5°, with no PageUp/PageDown or
|
||
Home/End. Fine for picking by eye; a panel that wants an exact hue has no coarse/fine step.
|
||
- CHORE/UI: the colour wheel reads the pointer's angle only, never its distance from the
|
||
centre (marked `ponytail:` in `ColorWidget.tsx`), so a colour takes two gestures — the ring,
|
||
then saturation. A saturation-by-radius disc would need a second axis on a control that can
|
||
announce one value.
|
||
- CHORE/DEMO: `seed_demo.py` still writes the Home dashboard as three titled sections
|
||
("Right now", "Energy and comfort", "Yield model"). The editor and the panel now read a
|
||
page's sections as one grid, so those headings are no longer drawn and the first editor
|
||
save collapses them into one section. Re-author `PAGES` as a single section, and decide
|
||
whether the three headings come back as markdown widgets or go for good.
|
||
- CHORE/UI: the widget-side segmented control in `widgets.tsx` (`DropdownWidget`,
|
||
`style: "segmented"`) and `Common/RangePicker.tsx` still carry their own copy of the shape
|
||
now in `ui/segmented.tsx`. Pointing them there needs a size prop first — the widget one is
|
||
a full-width pill with a 44px touch target, the editor one is `w-fit`/`text-xs`.
|
||
- CHORE/UI: `IconPicker` in `panels.tsx` has no filter field; the grid shows all of `ICONS`
|
||
at once. Add one when the map outgrows a popover.
|
||
- CHORE/UI: the delete-panel button in `PanelsDialog` has no confirmation, while unpairing —
|
||
the strictly less destructive action — now does.
|
||
- CHORE/UI: `DashboardMosaic.blocksOf` offsets sections unconditionally, while
|
||
`DashboardView.flatWidgets` skips the offset for an unarranged document. Cosmetic, in the
|
||
schematic preview only; the two want to be one function once the mosaic may import the
|
||
dashboard chunk.
|
||
- FEAT/API: `DashboardSummary` carries no `icon`, so the panels dialog cannot show which
|
||
glyph each assigned dashboard draws on the rail — the icon is only visible from that
|
||
dashboard's own settings panel. Setting it from the panels dialog would need a decision
|
||
about publishing an icon change on its own, since that dialog has no draft concept.
|
||
- CHORE/TEST: nothing guards "a dashboard with two pages or sections survives an editor
|
||
save". Verified by hand; the only runner is Playwright, so it wants a spec that PUTs a
|
||
two-page document, opens the editor, drags one widget and asserts the second page came
|
||
back untouched.
|
||
- CHORE/UI: the transmit pulse and the editor's selection ring are both a 2px inset
|
||
`--primary` ring, so a control selected in edit mode and one mid-publish look the same.
|
||
Only visible while editing.
|
||
- CHORE/UI: slider tick labels are laid out by percentage without measuring, so five
|
||
labels of four characters can crowd on a tile narrower than its default four columns.
|
||
A width-aware count needs a `ResizeObserver`.
|
||
- FEAT/API: neither `FlowSummary` nor `DashboardSummary` carries a modified time, so
|
||
Home's "recently modified" order is a proxy — drafts first, then version counter, then
|
||
name (`byRecency`, `Common/DashboardMosaic.tsx`). The store is git-backed, so an
|
||
`updated_at` on both summaries would make it exact.
|
||
- PERF/UI: the home mosaic reads each dashboard's document for its footprint, capped at
|
||
eight. An installation with dozens shows name-only tiles past that; a placement digest
|
||
on `DashboardSummary` is the fix.
|
||
- PERF/API: `_panel_may` re-reads `panels.json` and every published dashboard document of
|
||
the panel on each request a screen makes, to resolve the message allowlist. Marked
|
||
`# ponytail:` in `api/deps.py`; cache behind the dashboard store's version if it shows
|
||
up in a profile.
|
||
- CHORE/TEST: the frontend has no unit-test runner (Playwright only), so pure helpers like
|
||
the slider's `tickIntervals` have nowhere to be checked cheaply. `color.check.ts` is a
|
||
worked pattern — `bun run` over plain `node:assert`, no framework, imported by nothing so
|
||
it never bundles. What is left is a make target that runs every `*.check.ts`.
|
||
- FEAT/UI when an installation is added to the hub, the dialog which shows the access code should disappear automatically
|
||
- BUG/UI the "Connect" button in "Remote Access" when adding an installtion to the hub is invisible and only shows upon hovering (could also be a local browser issue)
|
||
- CHORE/PKG the SPA is not in the wheel: `fluksio serve` serves no UI, on the
|
||
assumption that a pip install is paired with a portal. Bundling `dist/` and
|
||
mounting it with `StaticFiles` would give a local dashboard — it needs a hatch
|
||
build hook running bun, `VITE_API_URL=""` for the same-origin case, and a
|
||
decision about the MCP `mount("/")` it would collide with.
|
||
- CHORE/PKG the Docker image still starts with `fastapi run`; `fluksio serve`
|
||
now does the same thing plus the bootstrap. Switching would give the container
|
||
and a pip install one code path.
|
||
- CHORE/DEPS `sentry-sdk` went to 2.x and the `requires-python` cap came off with
|
||
it. Nothing exercises Python 3.13/3.14 in CI — the matrix is one version.
|
||
|
||
- BUG/UI assimilate the design of the settings in the app to mirror the design of the settings in the portal
|
||
- BUG/UI: `CubeLoader` (`index/frontend/src/components/ui/cube-loader.tsx`; the index repo only, nothing in `app/` references it) paints nothing. Its `<path>` carries `className="n3xd-cube-line"` and the header comment points at that rule plus `n3xd-build-cube` keyframes "in index.css" — neither exists in `frontend/src/index.css` (nor in the index repo, which has the same component). Every pending state using it shows an invisible SVG. Related to the global loading animation below.
|
||
- 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.
|
||
- 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.
|
||
- CHORE/UI: two of the mobile-overflow floors are over-determined. Removing
|
||
`.widget-grid { min-width: 0 }`, or the segmented fieldset's `min-w-0`, leaves the mobile
|
||
suite green — the grid tracks are already `minmax(0, 1fr)` and the fieldset became a grid.
|
||
The uPlot legend is the one offender the assertion actually catches. Both are cheap
|
||
insurance for a future widget, but nothing would notice if they regressed.
|
||
- CHORE/API: artifact blobs are content-addressed and have no GC, so deleting a flow drops
|
||
its `run_artifact` rows and leaves the bytes on the data volume.
|
||
- 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.
|
||
`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.
|
||
(tests/flows.spec.ts, tests/admin.spec.ts)". There are nine.
|
||
- 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)
|
||
- CHORE/UI: the edge popover shows the same value twice — `MessageSparkline` falls through to a collapsed `ValuePreview` for a non-numeric value, and `EdgeInspector` then renders its own `ValuePreview defaultOpen` below it. Cosmetic; one of the two is redundant.
|
||
- FEAT/UI: a settings-and-inputs overview page, so what every node of an installation is configured with can be read and searched in one place rather than one panel at a time.
|
||
- FEAT/UI: an input endpoint opens the flow panel, which is right for editing but not for reading one value. A panel of its own — the declaration, the current value, its history — is what clicking a label wants to give.
|
||
- FEAT/UI: sync between the header of the python function and the node configuration. The config→header half exists for ports *and* settings — `scaffoldFor` writes `def process(<ports>, <settings>)` and `editNode` keeps it in step — but only while the source is still exactly the generated scaffold (`SCAFFOLD_SHAPE`), and never for shared code. What is missing is the same for code someone has edited, and the reverse direction: nothing parses a `def process(...)` header back into ports and settings.
|
||
- 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
|
||
- 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 introduce a graph panel which renders at the top right next to the graph view (to make more use of the horizontal space) and which allows (de-) selecting flows to be excluded from the graph view or search for individual nodes where only the flows containing this node should be shown (like slicing the brain)
|
||
- FEAT/UI labels in flows (indicating dashboard widget connections) naturally can't pulse. Instead add an animation (enlightning fade) from either ltr or rtl depending if the label is in- or outbound
|
||
- CHORE/UI: `layoutGraph` treats every node as 220×56 rather than measuring, because feeding a measurement back into the layout oscillates. A node wider than that crowds its neighbours; take the sizes from `node.measured` once they have settled if it shows.
|
||
- FEAT/UI/MOBILE: a rank of many nodes — a connector feeding eight dashboard tiles — is thousands of pixels wide however the graph is turned, so on a phone the fit shrinks it past reading. The layout is right and the flow is simply too big for the screen; a "one rank at a time" reading mode, or wrapping a wide rank, is what would make it legible.
|
||
- CHORE/UI: an edge's value chip sits at the bezier midpoint while the layout reserves its room at dagre's label rank. The two agree closely enough today; if chips ever pile up, take the position from the layout instead.
|
||
- 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
|
||
|
||
### 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.
|
||
- FEAT/NODE: the second WF-RAC unit (the one Node-RED addresses with operatorId "0") closes the connection on an anonymous read. It likely wants an account registered; the first unit answers without one.
|
||
- CHORE/NODE: `wfrac` sometimes reports `mode` as "unknown" while the unit is off, because the mode bits hold a value outside the known set. Narrower than it first looked: a unit switched off *by a command* keeps its last mode in those bits and reads back correctly, so this is about however the remote turns it off. "off" would still read better than "unknown".
|
||
- PERF/NODE: `ArtNetOut.write` sends one frame per input port, so a node with two ports emits two frames per run. The last one carries both channels, so the end state is right; folding them into one send would halve the traffic.
|
||
- CHORE/NODE: the Art-Net node starts from an all-zero universe and has no way to learn what the fixtures are currently at — Art-Net has no read-back. Taking over a universe therefore blanks everything the flow does not drive. A baseline setting, or driving every channel, is what a real cutover needs.
|
||
|
||
### 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,
|
||
so an alarm flow provides `safety.fire` and each actuator binds it with its
|
||
own safe value.
|
||
- CHORE/FLOW: the 433 MHz weather stations retain `status = dead`. Nothing
|
||
waits on them, but every humidity rule is inert until they are back.
|
||
- CHORE/INFRA: `TZ` now reaches the api container and is set to Europe/Berlin.
|
||
The container has to be recreated once before any cron means local time.
|
||
- CHORE/UI: a rollershutter is three buttons, because there is no cover widget
|
||
and no position to bind one to. A cover widget with an up/stop/down control
|
||
would be one tile instead of three, on four motors.
|
||
- PERF/FLOW: the MQTT node opens one connection per node, while
|
||
`docs/reference/node-types.md` says nodes sharing a broker share one. Eight
|
||
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
|
||
|
||
- CHORE/FLOW: `compile_check` sends the *draft* source under the running node's cache key, so the worker recompiles the published source on its next call. Correct, but one wasted compile per save on a busy node.
|
||
- FEAT/API: `POST /modules/apply` rebuilds the whole pipeline so a node that could not import its package stops being red. That resubscribes every MQTT node in the deployment; a targeted rebuild of the flows that actually failed to load would be gentler.
|
||
- CHORE/FLOW: a node's return value now round-trips through JSON, so tuples arrive downstream as lists and anything non-JSON is an explicit error. That is the message contract, but flows written before this may notice.
|
||
- BUG/FLOW: a node whose cold-start imports plus body exceed its timeout can never succeed. The timeout covers the first call's imports, a timeout kills the worker so the next attempt is cold again, and `compile()` only ever warms one of the N workers. Broadcasting `compile` to every worker is the candidate fix, at the cost of N module executions per reload.
|
||
- CHORE/FLOW: worker protocol loose ends — the request `id` is echoed but never checked, `json.dumps` runs twice per result (once to prove it is JSON, once to send it), `_remote_types` is an unbounded cache keyed on class names that user code chooses, and `PythonWorkerPool._lock` guards less than its name suggests.
|
||
|
||
### Engine history
|
||
|
||
- CHORE/FLOW: a rate-limit flush gets no run record — it is the tail of the run that scheduled it, and there is no id linking the two. A flush that fails therefore shows as a failure with no run beside it.
|
||
- CHORE/FLOW: `Pipeline.flush` releasing a held value runs its cascade without a run id, so those executions land in the minute rollups but in no run. Threading the scheduling run's id through the queue item would close it.
|
||
- CHORE/FLOW: `EventBus.emits` counts a node's publishes so a reconnecting client can restore what it missed. Two deliberate shortcuts: the increment is a read-modify-write, so two threads emitting from one node can lose a count — `publish` is documented as never blocking, and a dropped increment is invisible in an animation — and the dict is never pruned, so a deleted flow's node ids sit there until restart. Bounded by distinct ids seen in the process, and orphans are never read, since lookups go through `brain_graph` members.
|
||
- CHORE/API: the metrics collector is a bus subscriber, so a storm that overflows the bus queue undercounts. The events dropped are the same ones the websocket drops; exact accounting would need the collector to be fed from the engine rather than the bus.
|
||
- CHORE/UI: the Home block's "Changes" list is the newest 15 audit rows whatever range is selected. Deliberate — an audit trail is worth reading past the window — but it sits under a control that governs everything else on the screen.
|
||
- CHORE/FLOW: run records for a deleted flow stay until the retention window passes, so a flow that no longer exists keeps appearing in the history. Deliberate — it is a record of what ran — but `forget_flow` could offer to clear it.
|
||
- CHORE/API: nothing can ask the collector to flush now, so anything needing the tables to be current has to wait out `FLUSH_INTERVAL_S` — which is what the soak harness does before clearing its own rows.
|
||
- CHORE/FLOW: `RedisWorkQueue.clear_flow` deletes only `pipeline:__parked__:{flow}`, so a deleted or renamed flow's `__queue__` stream entries, `__delayed__` zset members and `__done__:*` markers stay behind. The stream is capped and the entries are dropped when they reach a node that no longer exists, so it costs work rather than correctness.
|
||
- CHORE/FLOW: `MemoryWorkQueue`'s in-flight count is a counter around claim/ack, and claiming already removed the item — so an item a handler leaves unacknowledged (no pipeline bound) counts as in flight until the process ends. Nothing can hand it back either way, which is what the memory queue is.
|
||
- CHORE/API: audit rows ride the same drop-oldest bus as telemetry, so a storm can lose one. Writing a node's source is not audited either; publishing is.
|
||
- 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.
|
||
|
||
- 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
|
||
`interval` on the request port is the backstop, and it belongs to the flow
|
||
serving the request rather than to the widget asking.
|
||
- CHORE/FLOW: one request/answer pair per InfluxDB node — the first input
|
||
carrying a `flux` key is the request and the answer leaves on the first output
|
||
port. A second query stream through one node needs a second node.
|
||
- CHORE/FLOW: porting the controls needs a declared writable message per control,
|
||
since an input widget can only target what a flow declares. Declaring one is no
|
||
longer API-only — the flow panel edits a flow's inputs and the canvas draws each
|
||
as a label — so the dashboard-input node this asked for has largely been
|
||
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
|
||
|
||
- 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
|
||
several whole dashboards instead, each with its own canvas and its own publish. `PageDef`/
|
||
`SectionDef` stay in the schema and still round-trip; the editor and the panel both read a
|
||
page's sections as one grid, and section headings are no longer drawn. Recorded, not open.
|
||
- 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. The per-panel nonce does not reach it either: `pnc` is only checked on a token this installation signed.
|
||
- 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.
|
||
- 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/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`'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.
|
||
|
||
### Flow editor follow-ups
|
||
|
||
- 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: 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.
|
||
|
||
### Infrastructure
|
||
|
||
- 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.
|
||
|
||
## Deferred
|
||
|
||
Open on purpose. Each names what should bring it back.
|
||
|
||
- PERF/UI: the app's entry chunk exceeds the warning threshold. React Flow and Monaco are already lazy; a manualChunks split measured no better, so this needs route-level work on the shell rather than chunking config.
|
||
- PERF/UI: the Monaco chunk is 2.6 MB. It only loads when a node panel opens, but the editor could be trimmed further or swapped for CodeMirror if that becomes a problem. NOTE: switch to codemirror; loading speed is definitely an issue.
|
||
- CHORE/API: node source saves carry no version precondition, so two clients editing the same node's code are last-writer-wins. The flow document is what the optimistic lock protects; code files would need their own, and an exact-match one produces false conflicts against a single client's own interleaved flow and source saves. Revisit with the M5 multi-user work.
|
||
- CHORE/FLOW: shared node sources bypass the draft/publish split. Editing one writes the library copy and reloads immediately, since the code is not any single flow's to hold back. Deliberate, but it means a shared node is the one thing publish does not gate.
|
||
- CHORE/INFRA: `requires-python` is capped below 3.14 because the MCP SDK wants a newer starlette there than the pinned `sentry-sdk<2` allows. Lift the cap when sentry-sdk moves to 2.x.
|
||
- 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: 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: 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. NOTE: remote workers landed without it — a worker dials the engine's own socket and never touches Redis, so the queue still does not cross a host. Reopen if a second engine ever pulls from the same stream.
|
||
- FEAT/RUNS: stage caching. `run_node.cache_key` is written on every run and the artifact store is content-addressed, so the pieces are in place; what is missing is computing the key from the node's source digest plus its input values and skipping a node whose key already has an `ok` row with its artifacts still present. The two research repos want this more than they want resume — neither persists checkpoints, and both re-run unchanged preprocessing every time.
|
||
- FEAT/RUNS: per-label requirements overlays (`requirements-gpu.txt`) synced into a remote worker's venv, with drift surfaced against the engine's manifest. Today a worker's environment is whatever `--python` points at, which is fine for one hand-managed GPU box and not for several. `venv_digest` already arrives at attach and is shown on `/workers`, so the reporting half exists.
|
||
- FEAT/UI: a dashboard shows a run's curve only while it is running. Emissions reach the socket live, but a run's values live in its own state namespace, so reloading the panel afterwards leaves the chart empty — the durable series is on the run (`/runs/{id}/metrics`) and nothing binds a widget to it. A chart variant that reads a run's series, or the existing querying chart pointed at `/runs/series/compare`, is what would close it. This is also what a demo needs to show a finished experiment rather than only a live one.
|
||
- FEAT/UI: launching a sweep is API-only. Pressing Run on a batch flow asks for its parameters, but the many-runs-at-once shape has no UI; a run detail screen is what it wants to land next to.
|
||
- FEAT/RUNS: a run detail screen. The API answers everything — params, per-node status with logs and tracebacks, artifacts, metrics, and `/runs/series/compare` in the chart widget's own `series` shape — but nothing in the dashboard reads it yet, so a run is inspected over HTTP. Comparing curves is a widget binding once someone builds the page around it.
|
||
- FEAT/RUNS: a thin client CLI (`fluksio run/runs/sweep/worker`) over the same API. The engine being resident is what makes runs cheap; a CLI is ergonomics on top, and `curl` covers it until someone is running sweeps daily.
|
||
- FEAT/RUNS: the step on a run's series is the count of emissions on that message, so a node yielding every tenth training step records steps 0, 1, 2 rather than 0, 10, 20 — a faithful x-axis of its own emissions, not of the loop inside it. If a real step number ever matters, a `record`-typed streaming port carrying its own `step` is the shape to read it from; the column is already there.
|
||
- CHORE/RUNS: an emission publishes on the node's port and, in a live flow, enqueues a cascade with no payload of its own — the value is already in state, and an item carrying it would re-apply that value whenever it was claimed, which is how a mid-node emission overwrites the one the node returned at the end. Downstream therefore reads what is current rather than the value that caused it to run. Right for a curve; worth revisiting if something ever needs every intermediate value delivered rather than sampled.
|
||
- CHORE/RUNS: `run_metric` has no retention. Deliberately outside `OBS_RETENTION_DAYS` — an experiment nobody deleted should not vanish on a rollup window — but a few thousand runs at 3000 steps will want a policy eventually, probably per-flow rather than global.
|
||
- CHORE/RUNS: a run holds one worker slot per node for its whole duration, and `MAX_PARALLEL` run drivers bound how many graphs are in flight. A sweep of 500 therefore queues behind the pool rather than the driver count. Fine — the GPU is the scarce thing — but the two limits are unrelated numbers that read as if they were one.
|
||
|
||
## Blocked
|
||
|
||
- CHORE/INFRA: `bun install` inside the frontend Docker build intermittently fails with
|
||
"Fail extracting tarball" for several packages at once, and succeeds on a plain rebuild.
|
||
It looks like concurrent extraction under memory pressure. Pin down or retry in the
|
||
Dockerfile if it starts costing CI time. NOTE: memory lifted; retry and close if stale
|
||
|
||
- CHORE/DOCS: `docs/` (the published site) documents `device` and `device_policy` as
|
||
API-only, because the node panel has no field for either. That is the one place the
|
||
public docs have to say "use the API instead of the UI". Adding a Device section to
|
||
`NodePanel.tsx` — a label field plus a require/prefer toggle — would close it.
|
||
- CHORE/DOCS: the site's node-type reference is hand-written from `NODE_TYPES` and each
|
||
node's `Params`. It will drift. `GET /flows/node-types` already returns the whole thing
|
||
with its schemas, so a generator (the way n3xd generates its command catalog) is the
|
||
obvious fix once the type list stops moving.
|
||
|
||
- `make update`/`make up` (production overlay) runs `up -d --remove-orphans` under the
|
||
same `fluksio-app` project as `make dev`, so it silently deletes the local Traefik
|
||
container `fluksio-app-proxy-1`. With `REVERSE_PROXY=external` and no host proxy,
|
||
the whole stack then has nothing bound to :80 and `*.localhost` stops resolving.
|
||
Either drop `--remove-orphans` or make `update` refuse when `ENVIRONMENT=local`.
|
||
|
||
- CHORE: `frontend/vite.config.js` and `frontend/vite.config.d.ts` are committed build
|
||
output of `vite.config.ts` and fail `biome check` as tracked (semicolons, 4-space
|
||
indent). The pre-commit hook therefore fails for anyone who stages any other file
|
||
under `frontend/`. Either gitignore the two or run the formatter over them once.
|
||
- CHORE: the pre-commit biome hook selects files by the `frontend/` prefix rather than by
|
||
extension, so a non-JS file placed there is handed to biome and blows up with an
|
||
internal error on a doubled path (`frontend/frontend/...`).
|