Skip to content

feat: layered location ownership model and transfer graph UX - #290

Merged
RyanTheRobothead merged 17 commits into
unstablefrom
location_ux
Apr 15, 2026
Merged

feat: layered location ownership model and transfer graph UX#290
RyanTheRobothead merged 17 commits into
unstablefrom
location_ux

Conversation

@RyanTheRobothead

@RyanTheRobothead RyanTheRobothead commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary

Implements the layered location ownership model and redesigns the transfer graph UI — the core changes needed to support node-managed vs lab-managed locations in MADSci.

Layered Location Ownership Model

  • Nodes can now own intrinsic locations (e.g., liquid handler deck slots, plate reader carriage) auto-created on node startup via the intrinsic_locations class variable on AbstractNodeModule
  • Lab-level config (locations.yaml) simplified to lab-managed locations, cross-node training, and location templates — no longer duplicates node-internal locations
  • New LabLocationConfig model with training section for teaching nodes (e.g., robot arm) how to access other nodes' intrinsic locations
  • New LocationManagement enum (NODE/LAB) and OwnershipInfo on Location model
  • Location reconciliation split: node-managed locations are authoritative from node, lab-managed from config
  • New location_migration.py and transfer_planner.py modules
  • Example lab fully converted to the new format with a new storage_rack_nest location template

Transfer Graph UX Redesign

  • New /transfer/graph/detailed endpoint returning node names per edge (TransferGraphDetailedEdge, TransferGraphDetailedResponse)
  • get_detailed_transfer_graph() added to LocationClient; detailed graph is cached alongside the simple transfer graph and rebuilt on location changes (avoids O(N²) recomputation on every poll)
  • Vue TransferGraph component redesigned with: location grouping by node, resource fill colors, edge tooltips with node/representation info, location index numbers
  • Transfer edges polled via store state
  • Extracted ManagedByFilter.vue component to deduplicate filter chip UI across layout variants

CLI/TUI Enhancements

  • madsci location list --managed-by node|lab filter option — managed_by query parameter is now validated against the LocationManagement enum (returns 422 for invalid values)
  • madsci location get displays Managed By and Owner fields
  • madsci location train command for adding node representations to locations
  • TUI LocationsScreen shows Managed By column and owner in detail panel

Code Quality Fixes

  • Resource ID capture in _sync_locations: Lab-config locations now correctly capture the resource_id from _initialize_location_resource() (previously discarded, causing perpetual "unresolved" state)
  • Health endpoint optimization: Added count_locations_by_managed_by() to state handler using count_documents() instead of deserializing all locations on every health check
  • Version-aware template sync: Template updates now use semantic version comparison (packaging.version.Version) — only upgrades are applied, preventing accidental downgrades
  • OTEL observability: Added span context to init_location endpoint (matching add_location pattern)
  • Defensive code cleanup: Removed unnecessary getattr(loc, "managed_by", None) patterns in CLI/TUI since managed_by is a required field with default

Other Changes

  • LocationsPanel in dashboard UI updated with improved layout
  • Updated location template documentation (10-location-templates.md)
  • Updated madsci-nodes skill documentation

Test plan

  • pytest passes (3985 tests, all green)
  • just checks passes (ruff, formatting, pre-commit)
  • Example lab starts with just up and locations reconcile correctly
  • Node-managed locations appear with managed_by=NODE after node registration
  • Transfer graph renders in the dashboard with node grouping and tooltips
  • madsci location list --managed-by node filters correctly
  • madsci location train deck_slot_1 robot_arm --overrides '{"x":1}' works

🤖 Generated with Claude Code

RyanTheRobothead and others added 14 commits April 9, 2026 16:49
Introduce a three-layer ownership model for locations that matches how
labs actually work: node-managed locations (intrinsic to hardware),
training (cross-node representation additions), and lab-managed locations
(defined in a reconcilable config file).

Phase 1 — Foundation:
- Add LocationManagement enum (NODE/LAB) and managed_by/owner fields on Location
- Add NodeIntrinsicLocationDefinition replacing NodeLocationTemplateDefinition
- Add intrinsic_location_handler() on AbstractNode with auto-prefix naming
- Add POST /location/init endpoint and init_location() client method
- Schema upgrade 2.0.0 → 3.0.0 with managed_by index
- Remove one-shot seed file mechanism

Phase 2 — Lab Config:
- Add LabLocationConfig and RepresentationTrainingEntry types
- Add lab_config_file setting (replaces seed_locations_file)
- Add _reconcile_lab_config() with mtime caching and training support

Phase 3 — Visibility:
- Add health fields (num_node/lab_managed_locations, last_reconciliation_at)
- Add ?managed_by filter on GET /locations
- Add GET /reconciliation/status endpoint

Phase 4 — User-Facing Surfaces:
- CLI: Managed By column, --managed-by filter, location train command
- TUI: Managed By column and detail panel fields
- Dashboard: management-type chips, filter controls, color-coded transfer graph
- Docs: rewrite location templates guide, CHANGELOG, skill updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Path.open() call prepares a file handle for httpx upload, not async
file I/O — suppress the ruff ASYNC240 warning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ck template

- Add platereader_carriage_repr representation template and intrinsic
  plate_carriage location to PlateReaderNode (now node-managed)
- Remove platereader_1.plate_carriage from lab-managed locations in
  locations.yaml, add training entry for robotarm_1 access instead
- Add storage_rack_nest location template to showcase location template
  functionality (transfer_arm role -> robotarm_wide_access)
- Fix reconciliation loop never starting: the lifespan that spawns the
  periodic reconciliation was only wired through create_app(), not the
  standard run_server() path. Moved lifespan into LocationManager as a
  create_server() override so it always runs.
- Add test for training applied to node-managed locations that arrive
  after startup (the exact scenario the reconciliation fix addresses)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…edResponse models

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… per edge

Adds a richer transfer graph endpoint that returns all node names capable
of executing each transfer and the minimum cost per location pair, enabling
clients to show which nodes service each edge in the transfer graph.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds client method for the GET /transfer/graph/detailed endpoint,
returning a TransferGraphDetailedResponse with edge-level node names
and minimum transfer costs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds transfer_edges ref, updateTransferEdges() function, initial call,
and 5-second polling interval to the Vue store, following the same
pattern as updateLocationTemplates/updateRepresentationTemplates.
Exports transfer_edges for use by consumer components.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l colors, and edge tooltips

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nsfer graph

- Derive human-readable node names from location representations keys
  instead of showing raw ULID owner IDs in group labels
- Add group-centroid repulsion force that pushes entire groups apart
  with minimum separation distance scaled by group size
- Increase intra-group attraction to keep groups cohesive
- Reduce transfer edge attraction to avoid overriding group separation
- Increase layout iterations from 150 to 200 for better convergence

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add numeric index inside each graph vertex, matching a new # column
  in the locations table (sorted alphabetically by name)
- Index text color adapts to fill brightness for contrast
- Increase group min separation (220 + 25*size) to reduce overlap
- Strengthen center gravity (0.01 -> 0.02) to better fill available space
- Reduce edge thickness (2 -> 1) and opacity (0.5 -> 0.3) to cut clutter
- Edges highlight on hover (2.5 width, 0.8 opacity)
- Default table sort by graph index for easy cross-reference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous commit only added the prop to the tabbed layout instance.
The stacked and responsive layout instances were missing it, so indices
never appeared on the graph vertices.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Apr 15, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  src/madsci_client/madsci/client
  location_client.py 184-190, 287-288, 889-890, 911-920
  src/madsci_client/madsci/client/cli/commands
  location.py
  src/madsci_client/madsci/client/cli/tui/screens
  locations.py 272-281, 320-326
  src/madsci_common/madsci/common/types
  location_types.py
  node_types.py
  src/madsci_location_manager/madsci/location_manager
  location_migration.py 254-255
  location_server.py 234-257, 460, 517-519, 1100, 1128-1129, 1147-1148, 1169-1170, 1186-1190, 1197-1198, 1231-1232, 1499-1500
  location_state_handler.py
  transfer_planner.py
  src/madsci_node_module/madsci/node_module
  abstract_node_module.py
Project Total  

This report was generated by python-coverage-comment-action

Resolve merge conflicts from CLI/TUI buildout branch integration:
- location.py: adopt _make_client context manager pattern, keep managed_by filter
- locations.py TUI: use model attribute access, keep managed_by/owner fields
- data_client.py: use unstable's cleaner file handle pattern
- test_tui_screens.py: keep location ownership tests with model-based API, include
  all migration tests from unstable
- Fix bare _make_client assignment in train command (missing context manager)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@RyanTheRobothead RyanTheRobothead changed the title feat: layered location ownership, transfer graph UX, CLI/TUI phase 2, httpx migration feat: layered location ownership model and transfer graph UX Apr 15, 2026
RyanTheRobothead and others added 2 commits April 15, 2026 10:02
- Fix _sync_locations discarding _initialize_location_resource return value
- Optimize health endpoint by adding count_locations_by_managed_by() to state handler
- Cache detailed transfer graph alongside simple graph in TransferPlanner
- Validate managed_by query parameter with LocationManagement enum type
- Prevent template version downgrades using semantic version comparison
- Add OTEL span to init_location endpoint
- Clean up unnecessary defensive getattr calls for managed_by field
- Extract duplicated managed-by filter chips into ManagedByFilter.vue component

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@RyanTheRobothead
RyanTheRobothead marked this pull request as ready for review April 15, 2026 14:45
@RyanTheRobothead
RyanTheRobothead merged commit 748180b into unstable Apr 15, 2026
4 checks passed
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