Files
app/NOTEPAD.md
T
stroblmeandClaude Opus 5 41b2c28b2b The e2e suite names its own origins, and refuses a live instance
`tests/utils/api.ts` took the API origin from `VITE_API_URL`, which
`tests/config.ts` loads out of `app/.env`. In a checkout configured for a
deployment that names the deployment — so the browser went to the local stack
while every setup and teardown call, `deleteAll` included, went to the live
one. `privateApi.ts` had the same reading, and it creates users.

Both origins now come from one place: `PLAYWRIGHT_BASE_URL`, with the API
derived from it (`app.<domain>` → `api.<domain>`) or named outright by
`PLAYWRIGHT_API_URL`, which is what CI and the compose service set. Nothing in
the suite reads `VITE_API_URL` any more.

Belt and braces, since a stack served under a real domain answers to the same
names its production instance does: a global setup resolves both origins and
refuses anything that is not loopback or a private range, before a test runs.
`PLAYWRIGHT_ALLOW_PUBLIC=1` says you meant it.

`make test-frontend` is now that safe run — the Playwright image on the proxy
network with both names mapped onto Traefik by address, as the host user so it
does not leave root-owned results behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUb8YpL2s3gmN9WTACTt4q
2026-08-20 20:46:04 +02:00

37 KiB
Raw Blame History

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

  • FEAT/UI center a node when being selected, so that the panel does not overdraw the node
  • 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 remove the zoom in/zoom out buttons from the toolbar; either we have a mouse or use touch gesture for that
  • 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)
  • FEAT/UI add animation to widgets; i.e. status of bars, gauges etc. should fade from one state to another. Multi-buttons (like "Mode" in the "Home" dashboard of the demo) should transition from one state to another; use inspiration for animations based on the google material guidelines
  • BUG when clicking "edit" in the "Home" dashboard of the demo on hub.fluksio.com, most of the panels disappear (only a handfull is left for actual edit)
  • 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
  • BUG/UI when enlarging the code editor of a node, the code editor should enlarge to the left (node settings remain on the right) so that the code editor fills the center of the screen with the node properties available next to it (still in the same panel, essentially just a second column). Note that this overdraws the toolbar and flow name box which should move out of the screen towards the bottom and top respectively
  • BUG/UX the console/log panel should be moved up slightly (to separate from the toolbar) and kept open (console button becomes stateful) and center over the toolbar
  • 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
  • CHORE/INFRA: make test-backend reads app/.env, so a checkout configured for a deployment fails two tests that assume the development defaults — test_private.py::test_create_user (the /private router is off unless ENVIRONMENT=local) and test_mcp_http.py::test_an_agent_can_list_and_call_tools (the MCP host allow-list is built from DOMAIN while the test speaks to api.localhost). Both pass under ENVIRONMENT=local DOMAIN=localhost; the suite should pin its own environment rather than inherit the deployment's.
  • CHORE/DOCS: app/development.md is still the upstream template's text — compose.override.yml, localhost.tiangolo.com, docker compose watch as the dev flow — so it disagrees with the Makefile about how the stack is started and says nothing about the Playwright suite. The containerised run above is what it should carry.
  • 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
  • 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: UplotChart pushes new readings only when the number of points changes (useEffect(..., [points, key])), so a refetch that returns the same count with different values leaves the chart drawing the old ones. Harmless while a window is still filling up — each minute adds a bucket — but a full rolling window keeps a constant count, and the chart then stops following the data.
  • 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.
  • CHORE/UI: the bar widget's nested fill is --chart-5 against a --primary outer fill, which measures 2.53:1 in light mode — under the 3:1 guideline for non-text. The inset-y-1 gutter supplies the edge, and every other slot of the ramp collapses in dark mode 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.

  • BUG/INFRA: scripts/backup.sh dumps Postgres only — the flow git repo, secrets.enc, oauth-key.pem (losing it invalidates every issued MCP token), alerts.json and the Redis AOF are not backed up. Extend it to tar app-flow-data and copy a BGSAVE'd Redis snapshot. Its header comment still claims "the flow engine keeps no on-disk state", which stopped being true when flows became a git repo.
  • CHORE/FLOW: state backend and work queue share the pipeline: prefix in db 0, so RedisState.clear() would DEL the work-queue stream and RedisState.keys() enumerates queue keys — only callers filtering __-prefixed names keep it safe. A separate prefix (or db index) for the queue removes the hazard.
  • 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.
  • CHORE/INFRA: the SQLAlchemy engine sets no pool_pre_ping, so long-idle connections throw once after a Postgres restart before the pool recovers.

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.

  • FEAT/NODE: the connectors only read. Enable the write paths with someone watching: WF-RAC setAirconStat (needs an operatorId registered with the unit first, which is itself a write) and Art-Net transmit.
  • 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 reports mode as "unknown" while the unit is off, because the mode bits hold a value outside the known set. Faithful to the reference decoder, but "off" would read better.

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.
  • 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.

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: nothing deletes a flow's Run rows. Deleting a flow clears its values and its history but leaves the runs, so a demo that is reseeded accumulates them.
  • 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/API: /observability/summary reports the work queue's depth as the Redis stream length, which is the journal size (capped at STREAM_MAXLEN) rather than a backlog. The health screen shows pending instead; the field name still invites the wrong reading.
  • PERF/API: the health block picks its window now, but /observability/timeseries and /observability/flows still read every metric_minute row in it and fold them in Python. bucket_s only coarsens what comes back, so the 7d preset pulls a week of rows on each 30 s poll. date_bin() is what makes the long windows cheap.
  • CHORE/API: /observability/summary still returns failures_24h, which nothing reads any more — the Home tile counts errors over the selected window from the rollups instead. Drop the field, or let the summary take a window.
  • 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: two unmarked ceilings — /observability/flows scans every metric_minute row in the window on each 30 s poll, and 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.

  • BUG/UI multiple entries in the bar diagram should draw stacked instad of drawn over another

  • 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.

  • FEAT/UI: the slider offers step now, but no tick labels — the datalist marks are unlabelled and drop out past fifty steps.

  • FEAT/UI: per-dashboard theme — forced light, forced dark, or switched on a schedule. View mode inherits localStorage and the OS preference today, which a panel in a room has no way to set. NOTE: to solve this, we could introduce a general message sending to the overall dashboard (so far we only treat widgets in a dashboard as a receiver). We could e.g. have a toggle in the dashboard settings which says "propagate theme" which enables a field for defining a consume input (identical to a standard node input) and then a node can connect to this property by producing a corresponding message. This would nicely generalize to other dashboard settings later. This could later also serve as a security mechanism, i.e. the possibility to lock down dashboards remotely

  • 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

  • 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. Enough for a screen in a house; an installation where a panel sits somewhere less trusted would want the narrower check.
  • 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: 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.
  • 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.
  • 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.
  • 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.
  • FEAT/UI: the flow graph should auto zoom-to fit when a panel is open and edges are created through assigning inputs/outputs. Declaring a flow-level input or output already refits, since it adds an endpoint and the effect watches external.nodes.length; editing a node's own ports does not, and neither does opening a panel.
  • 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.
  • BUG/UI: renderedNodes overwrites xyflow's own selected flag with id === selectedId, so a box-selection of several nodes is invisible even though delete and copy act on all of them.
  • 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.

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