Skip to content

V3 fable auto#22

Merged
vagkaratzas merged 118 commits into
v3from
v3-fable-auto
Jul 4, 2026
Merged

V3 fable auto#22
vagkaratzas merged 118 commits into
v3from
v3-fable-auto

Conversation

@vagkaratzas

Copy link
Copy Markdown
Collaborator

Fix Plan — Code Review Findings

Source: REPORT_CODE_REVIEW.md (commit 5f3a04a). One item = one commit. Check off when merged.

High

  • H1. Wire external session handoff — in frontend/src/main.ts, parse new URLSearchParams(location.search).get('session'); if present, call api.resolveExternal(token) and loadSession() after setup. E2E test: POST a session to /api/external, open returned URL, assert network loaded.
  • H2. Validate session referential integrity — in backend/app/services/session.py _validate: reject node.layer ∉ layer names and edge src/trg ∉ node ids (HTTP 400). Unit tests for both.
  • H3. Fix setChannelVisibility arrow lookup — in frontend/src/actions/edge.ts, replace positional children[j + 1] with lookup by userData.tag === channel + ArrowHelper type; toggle all matches, drop the fragile interleave assumption.

Medium

  • M1. Surface channel-removal warningsession.py _handle_channels: compute warning before popping channels; return warnings through SessionImportResponse and show in frontend file panel.
  • M2. Reject duplicate layer namessession.py _validate: error on duplicate layers[].name.
  • M3. Stop deriving source_node by slicingfrontend/src/actions/network.ts buildFromSession: resolve source/target node + layer from the session nodes list, not slice(0, -len-1) string surgery.
  • M4. Fix super-node repulsion at x == 0backend/app/services/clustering.py _super_node_coords: replace slope hack with direct vector scaling (x * F, y * F).
  • M5. Guard _sweep racebackend/app/routers/external.py: wrap per-file stat/remove in try/except OSError: continue.
  • M6. Single-edge layer topologybackend/app/services/topology.py: verify v2 intent for the len(layer_edges) < 2 skip; either compute for 1 edge or return a client-visible warning.
  • M7. Un-alias Edge.importedColorsfrontend/src/three/Edge.ts: this.importedColors = [...this.colors].

Low

  • L1. Dedicated lasso membership flagcanvas_controls.ts: track lasso-hit nodes in a Set instead of the opacity===0.5 sentinel (hover pollution).
  • L2. Skip undo/redo in text inputsevent_listeners.ts handleUndoRedo: return early when event.target is input/textarea/contentEditable.
  • L3. Don't arm axis keys with Ctrl heldcanvas_controls.ts keyPressed: ignore z/x/c when ctrlKey || metaKey.
  • L4. Single source of truth for theme — write theme changes to store (or drop AppState.currentTheme); remove module-local duplicate in themes.ts.
  • L5. Fallback for unknown channel colorEdge.decideColor: ctx.channelColors[...] ?? ctx.edgeDefaultColor.
  • L6. ARENA_PUBLIC_URL read per requestexternal.py: read env inside handler (or document the import-time constraint).

UI

  • U1. Fix unreadable select options — layout/clustering/local-layout/topology-metric <select class="form-select"> dropdowns (frontend/src/ui/layouts.ts) render light text on light background. Add explicit color/background-color for select.form-select and its options in frontend/src/style.css so options are readable in all themes.

vagkaratzas and others added 30 commits July 2, 2026 19:35
Replace networkx/scipy/python-louvain with python-igraph (same C core as
R igraph — all v2 algorithms port 1:1, see SPEC). Package management via
uv: deps in pyproject.toml + uv.lock, requirements.txt deleted. Remove
stale algorithms/ scaffold dirs (SPEC now uses flat service modules).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…1 complete)

Dev: docker-compose with uv + node images, hot reload both sides,
VITE_PROXY_TARGET for in-network proxying. Prod: multi-stage build,
nginx serves Vite dist + proxies /api to uvicorn on one image (v2
r-base Dockerfile replaced; v2 preserved via v2-legacy tag).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports config/server_variables.R + global_variables.R into app/config.py
(RColorBrewer Set3/Set1 palettes inlined as hex). Endpoint serves the
v2 handler_initializeGlobals payload plus node palette, floor defaults,
topology metric and no-edge-layout lists. static_variables.R was mutable
Shiny state, ui_variables.R a footer year — both obsolete. All four R
config files deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the parseUploadedNetwork() chain from functions/input.R: subset
legit columns, trim, dedup (channel-aware), scale weights into 0.1..1
via mapper(), build Node_Layer keys. Validation mirrors
isNetworkFormatValid() (missing mandatory columns, non-numeric weight,
empty channel) as HTTP 400; MAX_LAYERS/MAX_EDGES enforced at the API
boundary. 12 pytest cases incl. real aspirin_3channels.tsv fixture.

input.R deletion deferred to Phase 7 (still holds JSON import, session
export, attribute uploads). Added pandas-stubs; mypy target 3.12 for
numpy stub syntax.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports functions/igraph/layout.R + general.R graph construction:
- services/graph.py: channel filter, perLayer/allLayers/nodesPerLayers
  scopes, simplify (sum parallel weights, keep loops)
- services/layouts.py: registry of all 11 v2 UI layouts as Graph.layout_*
  calls, per-request RNG seed (v2 set.seed(123)), returns 2D [y,z]
- no-edge layouts (Circle/Grid/Random) place isolated layer nodes as
  extra vertices (simpler equivalent of v2 filterPseudoNetwork)
24 pytest cases. layout.R deleted; general.R kept as spec through Phase 6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports functions/igraph/cluster.R. Clustering is an optional step of
POST /api/layout (not a separate endpoint): detect communities
(Louvain/Walktrap/Fast Greedy/Label Propagation via Graph.community_*),
lay super-nodes out with the global algorithm repelled from origin, lay
each community's members locally (real intra edges + tiny all-pairs
edges so disconnected members spread), translate into place. Response
gains a clusters map. 8 new pytest cases; cluster.R deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports functions/igraph/topology.R: Degree (raw Graph.degree, not
normalized centrality), Clustering Coefficient (weighted local
transitivity, isolates=0), Betweenness (edge-direction toggle +
weights). Values mapped per subgraph into [0.5, 2.5] via mapper()
(constant → 1); raw values returned for the View Data table. 7 pytest
cases. topology.R + general.R deleted — functions/igraph/ fully ported.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- POST /api/session/import: ports parseUploadedJSON + isJSONValid
  (defaults for scene/layers/nodes/edges, channel drop, edge dedup,
  scramble flag). Loads real Arena3DwebApp_aspirin.json / export JSONs.
- POST /api/session/export: thin JSON download (stateless — frontend
  owns the state).
- POST /api/external + GET /api/external/{token}: session stored under
  a random token in tmp/ with 24h TTL sweep + path-traversal guard;
  resolves back through the same normalizer (SPEC §5).
- VR (functions/vr.R) DROPPED, not ported — depended on external
  bib.fleming.gr hosting + A-Frame, out of scope (see MIGRATION.md).

11 pytest cases. Deleted init.R, general.R, reset.R, render.R,
js_handling.R, vr.R. input.R kept: attribute-upload functions still
to port to a future /api/attributes endpoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Foundation singletons:
- bus/index.ts: typed pub/sub, unsubscribe handle (replaces rshiny bridge)
- store/index.ts: single AppState source of truth
- commands/base.ts: Command + CommandHistory (undo/redo, emits
  history:changed)
- api/client.ts: hand-written typed fetch wrappers mirroring backend
  Pydantic models (no codegen dependency) — network/layout/topology/
  session/external

5 Vitest tests; tsc + eslint clean (skipLibCheck for third-party stubs).
Verified live: config/network/layout return 200, OpenAPI exposes all 9
paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migrate Scene/Layer/Node/Edge from www/js/classes/ to typed
frontend/src/three/ on npm three r170.

Key finding: vendored www/js/three/three.js was byte-identical to
stock Three.js r117 (diff = only a missing trailing newline). SPEC's
"manually patched copy" claim is wrong — no core patches exist, so no
src/three/ subclasses were needed.

- constants.ts: static geometry + 280-color palette (verbatim from v2)
- runtime.ts: typed ctx singleton replacing v2 ambient globals
  (registries, screen bounds, edge tunables, color-priority flags).
  nodeGroups/layerGroups were new Map() but only bracket-accessed in
  v2 -> typed as plain records.
- Scene/Layer/Node/Edge.ts: faithful ports; only API change is
  THREE.Math -> THREE.MathUtils.
- three.test.ts: 16 Vitest tests across all four classes.

Deferred (ponytail-marked): Scene.rotate() DOM slider + Shiny glue and
Layer.getColor() picker DOM read -> Phase 11 canvas_controls.

Delete www/js/classes/ + three.js. Keep matrix4.js/drag_controls.js
as living spec until Phase 11.

tsc + eslint + prettier clean; 21/21 vitest pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement the 8 Command classes in src/commands/scene.ts, each
capturing before/after state on the Phase 9 Three.js object model so
CommandHistory can undo/redo:

- ChangeNodeColorCommand / ChangeNodeSizeCommand
- ChangeEdgeColorCommand (captures colors + edgeFileColorPriority)
- MoveLayerCommand (position/rotation/scale; redraws inter-layer edges)
- ApplyLayoutCommand (positions + optional cluster IDs/colors, one step)
- ApplyTopologyCommand (node scales)
- ChangeThemeCommand (store-only via injected getter/setter)
- LoadNetworkCommand (build closure + ctx registry snapshot for undo)

Commands are thin: they apply/reverse already-decided values. API
fetch and position/scale math is the Phase 11 actions' job, which will
construct these commands with the results.

Supporting changes:
- runtime.ts: add ctx.edgeObjects registry + snapshotRegistries /
  restoreRegistries helpers (RegistrySnapshot).
- bus: add layer:moved event.

9 Vitest tests (execute/undo/redo per command). tsc + eslint clean;
30/30 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port the portable, unit-testable slice of the v2 object_actions:
src/actions/layout.ts — executeLayout coordinate normalization plus
applyLayout / applyTopology, which fetch from the API and dispatch the
Phase 10 ApplyLayoutCommand / ApplyTopologyCommand (one undo step each).

Confirmed against backend: /api/layout returns raw igraph [y,z] coords
(no layer-width normalization), so the frontend still performs the v2
mapping into [-minWidth/2, minWidth/2] * layer scale. Topology scales
are already mapped server-side and applied verbatim.

Re-scope the rest of Phase 11: the remaining 9 action files are coupled
to the renderer, camera, animate() loop, raycaster, DragControls, CSS2D
labels, and the UI panels — all built in Phases 12-13. They move to
those phases (documented in PLAN.md) rather than being stubbed now, and
www/js/object_actions/ deletion moves with them.

ponytail: allLayers scope only; perLayer/local layout refinements
deferred to the Phase 13 scope UI.

5 Vitest tests (normalize + applyLayout + applyTopology). tsc + eslint
clean; 35/35 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stand up the render spine so the app draws its first pixels. Verified
live with playwright-cli: /api/config loads, the WebGL canvas mounts,
and the tilted coordinate axes render.

- src/actions/screen.ts: port of screen.js — renderer, camera
  (orthographic), window bounds, raycaster, FPS-limited animate() loop.
  Shiny sync calls dropped. (screen.js was deferred out of Phase 11 for
  exactly this dependency.)
- src/main.ts: port of on_page_load.js — config fetch -> renderer/
  camera/scene setup -> mount canvas into #app -> animate. Exposes
  window.__arena = { ctx, history } so Playwright can read 3D scene
  state via page.evaluate (WebGL is opaque to the a11y tree).
- src/event_listeners.ts: window resize + Ctrl+Z / Ctrl+Shift+Z /
  Ctrl+Y undo/redo wired to CommandHistory.
- src/utils.ts: port of general.js (pure helpers), 6 Vitest tests.
- runtime.ts: add ctx.renderer/camera/fps + screen bounds (persist
  across network reloads; not reset by resetContext).

Deleted www/js/general.js (fully ported + tested).

Deferred: the canvas mouse/keyboard scene controls (clickDown/Drag/Up,
sceneZoom, keyPressed, right-click) land with canvas_controls (still in
the Phase 11 list). Documented in PLAN.md.

Updated PLAN.md Phase 13 to drive/verify DOM panels with playwright-cli
(@playwright/cli, installed) against the window.__arena hook.

tsc + eslint clean; 41/41 vitest pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…artial)

buildNetwork consumes backend-parsed NetworkData: fresh Scene + ctx
registries, layers spread on x, nodes scrambled per layer, edges grouped
per src---trg pair with channel colors. loadNetwork wraps it in
LoadNetworkCommand so an upload is one undo step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port of www/js/object_actions/node.js pure parts: raycaster hover,
double-click/search/select-all selection synced to the store, repaintNode
(cluster branch folded into Node.getColor), label-flag priorities using
Layer.isVisible instead of DOM checkboxes, shape/color-priority setters.
scrambleNodes moves here from network.ts. ctx gains lastHoveredNodeIndex,
selectedNodeColorFlag and the two node-label flags.

DOM-bound pieces (search bar, sliders, lasso, node description div,
attribute upload) stay with canvas_controls.ts / Phase 13 UI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 12 partial)

Port of www/js/object_actions/edge.js pure parts: renderInterLayerEdges
now runs every animate() tick (remove while dragging/paused/invisible,
v2 locked-flag redraw pair), channel color/visibility, unselectAllEdges,
and the edge-setting setters (direction, arrow sizes, width-by-weight,
opacities, color priorities, curvatures). redrawIntra/InterLayerEdges
move here from commands/scene.ts private helpers. ctx gains the four
inter-layer render flags.

Channel UI lists/picker stay with Phase 13; edge attributes upload with
POST /api/attributes; edge build already lives in network.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…12 partial)

Port of www/js/object_actions/layer.js pure parts: initialSpreadLayers
moves here from network.ts, raycaster hover with red highlight + repaint,
double-click/selectLayer/select-all selection synced to the store,
repaintLayers with default/picker color priority, per-layer visibility
and node-label toggles that recompute node label flags, plus the
coords/opacity/wireframe/color-priority setters. ctx gains
lastHoveredLayerIndex, hoveredLayerPaintedFlag, renderLayerLabelsFlag.

Checkbox group + color-picker DOM stay with Phase 13; DragControls and
the held-key/slider rotate/move/scale intervals with canvas_controls.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port of www/js/object_actions/themes.js: THEMES presets (light/dark/gray
from the v2 buttons), applyTheme sets renderer clear color,
ctx.edgeDefaultColor/labelColor, reassigns channel colors from the config
palette and repaints layers picker-priority with the theme floor color.
registerThemeListener() subscribes to theme:changed in main.ts, so
ChangeThemeCommand execute/undo now performs the concrete recolour.

Supporting: ctx.labelColor (v2 globalLabelColor), repaintLayers gains an
optional pickerColor override, assignChannelColorsFromPalette lands in
edge.ts. Theme buttons DOM + channel edit list stay with Phase 13; label
recolour with labels.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port of www/js/object_actions/labels.js keeping v2's plain
absolutely-positioned divs in #labelDiv (no CSS2DRenderer dep; inline
position styling until the Phase 13 stylesheet). createLabels runs on
network build; flag-gated renderLayerLabels/renderNodeLabels register as
animate hooks via a new registerAnimateHook in screen.ts, which avoids a
screen->labels->node->screen import cycle. showLayer/NodeLabels modes,
label resize and setLabelColor ported; themes.ts now recolors labels on
theme change and decideNodeLabelFlags raises the node render flag as v2
did. ctx gains renderNodeLabelsFlag + the two layer-label flags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port of the canvas half of www/js/event_listeners.js plus the held-key
and lasso helpers it pulled from node.js/layer.js: wheel zoom, z/x/c
axis keys + arrow-key pan (modern event.key), clickDown/Drag/Up with
left-drag pan, middle-drag orbit, held-key node-translate/layer-rotate,
shift-lasso rectangle select (dim to 0.5, select on mouse-up), and
dblClick with v2's node -> layer -> unselect-all priority.
registerCanvasControls() wires the listeners onto the mounted canvas
from main.ts (tabIndex for keydown, scroll/autoscroll prevention,
mouseleave button release). Shiny syncs and 100ms debounces dropped.

Right-click node menu stays with right_click_menu.ts; DragControls layer
dragging and the nav-button/slider table with Phase 13.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…artial)

Port of www/js/object_actions/right_click_menu.js plus the context-menu
builder from event_listeners.js: selectNeighbors, selectMultiLayerPath
(v2 starting/current-layer exclusions), recursive selectDownstreamPath
over inter-layer edges, and the executeCommand tail (render flags, label
flags, store sync). The <select> menu is placed over the clicked node in
#labelDiv; Link opens the node URL. Wired to the canvas contextmenu
event and removed on clickUp in canvas_controls, as v2 did.

Loader spinner + description panel content stay with Phase 13 UI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…x4.js

Hand-port of v2's tweaked three.js DragControls copy onto npm three math
(the matrix4.js shim goes away). v2 behavior kept: drag engages only
while the left button is held over a hovered layer plane, label render
flags raised while dragging, camera-facing drag plane with parent-space
offset. On release it now raises the inter-layer edge render flag and
emits layer:moved (v2 routed this through Shiny syncs). The planes list
is read live from ctx so a single registration survives network reloads
(v2 recreated DragControls per load). Registered from main.ts.

Both v2 living-spec files deleted; MIGRATION.md rows ticked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ject_actions)

screen.js, labels.js, themes.js, right_click_menu.js,
event_listeners.js and on_page_load.js are fully covered by the ported
TypeScript actions and main.ts; MIGRATION.md rows ticked.

Kept as living spec for their unported remainders: layout.js
(predefined/star/cube layouts), network.js (importNetwork session JSON),
node.js/edge.js/layer.js/canvas_controls.js (Phase 13 DOM), config/,
rshiny_handlers.js/rshiny_update.js (map to Phase 13 panels).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildFromSession consumes the backend-normalized SessionData (now typed
in api/client.ts): scene position/scale/rotation + renderer clear color,
full layer transforms (initial spread only when the backend
generate_coordinates flag says the JSON carried none), nodes with
positions/scale/color/url/descr (scramble per flag within min layer
width), edges with per-row file colors and opacity as the weight, and
the v2 setJSONExtras flags (label color, direction, width-by-weight,
edgeFileColorPriority). loadSession wraps it in LoadNetworkCommand as
one undo step. createEdgeObjects gains the v2 decideEdgeColors rule: a
row's own color wins over the channel palette. The store receives a
NetworkData equivalent so later layout/topology calls work on imported
sessions too.

network.js is now fully ported and deleted; MIGRATION.md ticked.
Session export + File-panel wiring land with Phase 13.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e 12 partial)

Port of v2 layout.js applyPredefinedLayout: scene tilt + layer reset
(planes re-face the camera via its quaternion), parallel/zigZag spreads,
starLike petals (each layer rotated its share of 360 degrees and pushed
outward by half its width + 100), and the cube arrangement (6 layers
per cube side, extra cubes offset along x). Direct layer transforms as
in v2 (not undoable); raises the inter-edge + layer-label render flags
in place of the v2 Shiny syncs.

layout.js stays as living spec for the perLayer/local normalization
refinements only (Phase 13 scope UI).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
collectSession assembles the export dict from live ctx state — scene
pan/scale/rotation + renderer clear color, layer transforms, nodes, one
edge row per channel with decideColor(j, forExport=true), and the
setJSONExtras flags (label color, direction, edgeOpacityByWeight) — as
v2 did through rshiny_update.js. exportSession POSTs it to
/api/session/export and triggers a blob download; api.exportSession
returns the Blob. Round-trip tested against buildFromSession.

File-panel upload/download buttons land with Phase 13.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vagkaratzas and others added 28 commits July 4, 2026 15:18
Action-panel drawers (File/Layouts/Scene/.../Data) could only be
dismissed by navigating back to Main View through the navbar tabs.
Add a small chevron handle docked to the drawer's left seam: '>'
collapses the open drawer to the canvas, '<' reopens the last one
that was open.

Derives the active pane from the shown.bs.tab event's own
data-bs-target rather than querying '.tab-pane.show': Bootstrap fires
that event as soon as the (unanimated) nav button activates, before
the pane's own fade transition adds its 'show' class, so a DOM query
at that instant could still match the pane being deactivated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
getRandomArbitrary drew from Math.random(), so every Load Example or
upload scattered nodes to a different layout. Back it with a seeded
mulberry32 PRNG and re-seed at the start of scrambleNodes with a fixed
seed (123, mirroring the backend's random.seed(123)), so the same
network always produces the same layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After a network upload, Load Example, or session import succeeds, switch
to the empty Main View pane so the drawer closes and the 3D scene is
revealed without a manual tab change. The panelDrawerToggle picks up the
change through its existing shown.bs.tab listener and shows the closed
state. On error the drawer stays open so the message is visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Buttons attach top-right on first network load and dispatch
ChangeThemeCommand, so theme switches are undoable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
applyTheme set layer colors via repaintLayers(theme.floor) but never
updated the #floor_color input, which is the source of truth on the
picker priority path. Hover-exit repaint (repaintLayers() / Layer.getColor())
read the stale input and reverted layers to the dark default. Restore the
v2 behaviour of writing the theme floor into #floor_color.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full-codebase review: 3 high, 7 medium, 6 low findings across
backend session/external/clustering/topology and frontend
edge/session/canvas-controls code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Checkbox tracker for REPORT_CODE_REVIEW.md items plus unreadable
select-option font color fix (U1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
main.ts now reads the ?session=<token> query param on load, resolves
it through GET /api/external/{token}, and loads the returned session.
resolveExternal typed as SessionData.

Closes H1 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reject sessions whose node.layer is not a known layer, or whose edge
src/trg is not a known node id, with a 400 instead of letting the
frontend crash on undefined.addNode / nodeObjects[-1]. Covers the
third-party /api/external payload path. Adds two unit tests.

Closes H2 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Match every child with userData.tag === channel instead of assuming a
line/arrow interleave. Arrows are skipped for zero-opacity channels, so
the old children[j+1] lookup hid the wrong sibling. Adds a regression
test with a missing-arrow sibling channel.

Closes H3 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Record the warning before popping channels (the post-pop guard was
always false, so it never fired), thread warnings through
SessionImportResponse, and show them in the file panel status line.

Closes M1 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Duplicate names collapse the frontend layerGroups map, parenting the
earlier layer's nodes onto the wrong plane. Fail with a 400 instead.

Closes M2 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildFromSession derived source_node/target_node by chopping the layer
suffix off the id, which mangles names that contain '_<layer>' and
produced garbage when nodeGroups missed. Use a nodeId -> node map
instead; backend referential validation guarantees the lookup hits.

Closes M3 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The slope-based form collapsed a community to the origin whenever its
super-node had x == 0 (common with Circle/Grid layouts) — the opposite
of repelling it. Scale the vector directly; identical to the old form
for x != 0.

Closes M4 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wrap the per-file getmtime/remove in try/except OSError so a file
removed by a concurrent request between listdir and stat no longer
500s /api/external.

Closes M5 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One edge is a valid 2-node graph; the < 2 guard (with its incorrect
'cannot form a graph' comment) silently dropped those layers from
scaling. Skip only truly empty scopes.

Closes M6 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
importedColors shared the same array reference as colors, so a future
single-array mutation would silently corrupt the file-color-priority
baseline and undo. Clone it.

Closes M7 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hover also dims a node to 0.5, so a node hovered at mouse-up outside
the lasso rectangle was selected. Track covered nodes explicitly.

Closes L1 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Guard the global undo/redo handler so keystrokes inside input,
textarea, or contentEditable elements edit the text instead of
undoing scene commands.

Closes L2 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ctrl+Z with the canvas focused armed axis-transform mode; a following
drag moved nodes/layers unintentionally. Skip axis arming on modified
keydowns.

Closes L3 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The module-local currentTheme shadowed store.currentTheme, which was
never written. Route the ChangeThemeCommand getter/setter through the
store so there is one source of truth.

Closes L4 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An edge channel missing from ctx.channelColors produced
new THREE.Color(undefined) -> silent white. Fall back to the edge
default color.

Closes L5 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reading it at import time froze the value; env changes (redeploy,
tests) were ignored. Read it inside the handler.

Closes L6 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The native option popup rendered with the OS default light background
while inheriting light --arena-text, leaving layout/clustering/topology
select options light-on-light. Set an opaque dark background on
.form-select option/optgroup.

Closes U1 in PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vagkaratzas vagkaratzas merged commit 5c29ec7 into v3 Jul 4, 2026
2 of 3 checks passed
@vagkaratzas vagkaratzas deleted the v3-fable-auto branch July 4, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant