- Live destination peer cards (ResourceNode standalone branch) get the
shared 64px card height instead of the old py-1 metrics
- Live standalone resource cards show "Name - Network" inline like
draft (only drilled views keep hiding it - the header names it there)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Discard changes to X?" read like dismissing the popup — the dialogs now
state the situation ("You have unsaved member/group changes") and spell
out that closing the panel loses the toggled assignments; the confirm
stays the short "Discard".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tab/search changes no longer remount the Virtuoso list (which painted
empty for a frame while measuring) — the mounted list scrolls back to
the top imperatively instead; the open/close remount for fresh
measurements stays.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Group/peer panels: no remount on target switch (motion key removed,
placement kept) — the slide-in only plays when opening from closed;
search/tab/selection still reset per target
- Peers, resources, and groups lists virtualize with react-virtuoso
through the styled scroll area (shared PanelVirtuosoScroller); lists
remount per target so scroll offsets don't carry over
- Edges (and their wide interaction path) get pointer-events: none —
no edge has a click action, so dragging over connection lines now
pans the canvas; replaces the dimmed-only rule
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Rename Focus to "Highlight Connections" (Waypoints icon): context menu
item, header pill text, exit aria-label. No ring on the highlighted
node (the dim is the signal) and no menu entry on it while active
- Availability relaxed: any node with an edge, as long as the canvas has
at least two policies
- Dimmed edges get pointer-events: none (incl. the wide interaction
path) so drag-to-pan works over them in highlight mode
- Draft entry from peer view: adopt live twin sizes BEFORE the build
layout — its overlap pass measured fresh nodes with an 80px fallback
and shoved the 60px-pitch policy column apart (huge gaps until Auto
Arrange re-ran on measured nodes)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Extract the peer page's Edit Peer Name modal into shared
EditPeerNameModal and reuse it from the control-center context menu
(existing peers only, permission-gated); saving PUTs the peer,
patches canvas nodes in place, and revalidates /peers
- User-device setup modal: "Select" -> "Assign" (header, step 2, button)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Peer, placeholder, select-peer, and select-user nodes all match the
group node's 64px height
- SelectGroupNode padding py-3 -> py-3.5 (same as GroupNode)
- DeviceCard default variant adopts the placeholder card's tighter
title/subtitle metrics so installed and pending peers read the same
- User select avatar on the peer icon-box surface (nb-gray-850)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Placeholder peers (user device / server / agent):
- Track pending installs as install-peer changeset entries: own "Install"
section in Review & Deploy, resolved by install/select or removal,
renames follow, deploy skips them (user steps, not API calls)
- User-device setup stepper modal (install NetBird -> select peer);
the canvas node drops its inline select, upgraded placeholders always
become plain peer cards
- Placeholders open the peer groups panel (click or Details); assigned
existing groups ride on the generated setup key as auto_groups
- Group panel lists absorbed draft members (held on the group node) with
NEW badge, IP/address placeholders, and alert CTAs: "Install or assign"
/ "Install" / "No network" (network picker works node-less via
assignHeldResourceToNetwork)
- Install hostname matching also scans group-held placeholders
Groups & policies:
- Group renames and membership changes propagate into the group copies
held by policy nodes/edges (policy-group-sync) so the edit modal and
PeerGroupSelector stay current
- New side groups added while editing a policy join their column
(same x, stacked below) instead of landing at the origin
- Group names: "All" reserved, duplicates rejected against canvas drafts
- Dropping into "All" rejected; All still mirrors draft peers added to
other groups (implicit membership, never deployed)
- No-network draft resources can join groups
- Delete/Backspace routes through the context menu's Remove semantics
(useNodeRemoval) instead of React Flow's raw deletion
Review & Deploy:
- Canvas warnings for undeployable states (placeholder-peer policies,
unassigned resources); modal stays open on partial failure so a retry
resumes; update-group deploys against fresh API state
- Focus mode is explicit only (context menu / header tool) - left-click
opens panels without dimming
- Drop-eligible groups show a white border mid-drag
Tests & docs: interaction matrix spec (specs/interaction-matrix.md),
three Playwright suites (lifecycle, connections, full matrix) on a shared
e2e helper, testids across draft controls, unit tests for canvas
warnings and policy-group-sync.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live resources open the networks page's resource modal on click (PUT on
save) and their context menu offers Focus / Edit / Disable — every
action behind the live-mode warning; the canvas patches from the
response. Peer context menus gain Details (opens the groups panel);
live group Delete is dropped (Focus / Details / Rename remain). Dialog
confirms gain an opt-in dismissOnOutsideClick, enabled for all
control-center warnings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clicking a peer opens the peer-side twin of the group panel — one Groups
tab listing every group ("All" excluded, draft-created ones included)
with assigned-first frozen order, search, and batch apply: draft stages
adds/removes into the changeset (canvas group nodes update in place),
live PUTs each changed group behind one warning confirmation. Same
chrome and close semantics (ESC keycap, discard guard, pane click), and
the clicked peer wears the panel halo — selectedPeerPanel lives in the
narrow DestinationGroup context so PeerNode can ring without
subscribing to CanvasState.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The live single-network view now runs its resource column through
orderFrameResources, so the drilled view, the draft drill-down and the
overview frames all agree (policy-reached resources first). The network
selectors (live + draft drill-down) size to their longest option label,
clamped to the old 256px as max.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Focus Mode is entered from the node context menus ("Focus", shown only
where it declutters — 4+ edges and 2+ policies in the neighborhood, via
isFocusWorthy) or the "F" shortcut; the header button is gone and the
top-center pill owns the key handling. Live right-click now also covers
groups (Details / Rename / Delete behind live-mode warning confirmations,
PUT/DELETE + canvas patch) and peers/resources (Focus).
The shared drilled layout sorts both group columns by name so the live
single-network view and the draft drill-down agree on the order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Focus Mode is now armed from a header button next to the mode switcher
(SquareDashedMousePointer, sky accents, "F", switcher-style shell): the
next node click picks the focus target — hover previews the ring, node
buttons are inert, selectors/edge-less nodes are unfocusable — then the
mode goes sticky: the target keeps a glowing sky ring (cc-focus-root),
on-path nodes stay clickable/movable (dimmed ones lock), group panels
open without stealing focus, and only Esc / the top-center pill's X /
the button exit. The pill (switcher surface, fully rounded) names the
focused node. Fit-view moves to "1" to free F.
Policy left-click opens the editor again (live and draft); the context
menu keeps Edit/Disable. Network frames order policy-reached resources
first, via one shared helper so live and draft frames agree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The destination group panel becomes a search + Peers/Resources tabs
editor (PeerGroupSelector-style rows and checkboxes): toggles edit a
local selection previewed on the node's count text, and Assign (draft)
stages ONE coalesced update-group change while Save (live) PUTs behind
the live-mode warning. Implicit closes (Esc, keycap, pane click) confirm
before discarding unassigned toggles via a shared close guard; Cancel
discards directly. The changeset/deploy pipeline learns member REMOVALS
(removedPeerIds/removedResourceIds + removedMembers node sets).
Panel chrome: components-panel surface, right-anchored full-height with
header-width tracking (in-place on width changes — no flash), toolbar
spring slide-in, singleton mount, frozen row order per open, and the
canvas pans the group node clear of the panel. Focus mode is skipped for
nodes without connections, and mode switches close the panel and drop
node selection.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Virtuoso keeps its scroll offset across data swaps, so changing the
category (or entering/leaving search) landed mid-list at the previous
tab's position. Remount the list on category/search-mode changes, like
the existing open/close remount.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The draft build centered the frame grid on the measured midpoint of the
source/policy columns, which always landed a few px off the live
overview's fixed midline — and the live-frame anchor shift then pushed
that delta onto the columns, so peer/policy cards hopped a few pixels on
every live↔draft switch. Use the same fixed midline as live
(SOURCE_NODE_HALF_HEIGHT) so the geometry matches exactly and nothing
moves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Policies move from x 480 / -17 center offset to the shared x 500 / +14
anchor, sources drop their -30 half-height shift, and the frame grid
centers on the columns' midline instead — so a policy and its source
keep their world position when switching between the peer/user/group
and networks views. The draft build layout mirrors the same change to
keep entry/Auto Arrange parity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Focus mode turns global nodesDraggable off so dragging pans, which also
froze the focused node itself. The dim effect now stamps a per-node
draggable: true on the focus root (overrides the global flag) and strips
it when focus ends — works for policy/peer/resource focus and the group
panel alike.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clicking a policy or peer now enters focus mode (path highlight + blue
ring) in live and draft instead of opening the policy modal. Policy
editing moves to the right-click menu: in live it offers Edit and
Disable/Enable only — both behind a "you are in live mode"
confirmation — applying via updatePolicy and patching the canvas in
place through refreshLiveViewRef. No Delete in live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On a controlled flow, instance.setNodes round-trips through
applyNodeChanges, which keeps replaced nodes at their ORIGINAL index —
so reparenting a resource that predates its frame (network drop
adoption, assign-to-network) left the child in front of the frame and
ReactFlow dropped the containment: correct parentId in state, card
rendered detached at frame-relative coordinates.
Reconcile the invariant centrally in onNodesChange (and heal persisted
snapshots saved with the bad order on draft restore). Also stamp the
owning network on policy-drop destination resources so they don't show
"No Network".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assigning a resource to a network that isn't a frame yet stamps only the
ref; dropping that network later now adopts those resources into the
frame (and the create-network assign waits for the frame to reach the
ReactFlow store instead of racing it on a 0ms timeout).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stragglers outside every layout column (e.g. a standalone resource in a
frames draft, type-bucketed at x 1400 inside the frame grid) could land
on top of another node. Add resolveNodeOverlaps to the shared draft
build layout: larger nodes anchor, smaller intersecting nodes are nudged
the shortest way clear plus a 40px margin, re-checked until free.
Detection is exact intersection so tight column rhythms keep entry-layout
parity. Unit-tested.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extract the draft build layout into utils/draft-build-layout.ts
(applyDraftBuildLayout) and use it for the toolbar's Auto Arrange, so
arranging an untouched draft matches the entry layout exactly for drafts
entered from any live view; drill-downs re-run the shared drilled layout.
Delete the divergent applyDraftArrangeLayout and its height-aware pitch
machinery.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Footer buttons follow the shared modal pattern (right-aligned, gap-3)
with the header's separator color; CTA renamed Save -> Assign Network.
- Network select: search inside the popover, popover matches trigger
width, list capped at 190px, placeholder "Select or create a
network...".
- New optional `footer` slot on SelectDropdown: content pinned below the
scrollable options (always visible regardless of scroll/search) — used
for "Create New Network", styled like a regular entry.
- Creating a network opens the Add Network form ON TOP of the picker
(nested modal); Cancel returns to the picker instead of closing both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
onBeforeDelete on the ReactFlow canvas: live mode blocks Backspace/
Delete entirely (the canvas mirrors the account); draft mode keeps node
deletion (edges cascade with their endpoints) but never deletes a
selected connection on its own.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The networks view stacked source and policy node TOPS on the column
midline while the frame grid centers frames on it, leaving a lone peer
~30px and a policy pill ~17px below their frame. Both the live overview
and the draft rebuild now subtract shared half-height constants.
Draft fixes this exposed: packFrameGrid also runs for a single frame
(previously left at the far layout column, so the live-anchor shift
dragged the columns ~350px out of line), and the frame-grid midline
falls back to the live twin's measured height instead of an 80px guess.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Source columns (live peer/group/user views, user-view peers, draft
rebuild) share the destination column's 100px pitch instead of 80/120.
- Live destination resources show "Name - Network" inline like draft
(network ref stamped in addDestinationResourceNodes).
- Draft rebuild: dual-role groups (source AND destination) restack in
the destination column like the live view shows them.
- Mode-switch flicker: React Flow hides unmeasured nodes for one frame,
blanking the swapped canvas. Draft nodes now adopt their live twin's
measured size (initialWidth/Height), including renamed ids (self-ref
dest-group clones, destination resources/peers).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Policies were matched by rule.sources only, so a destination-only group
rendered an empty view. Those policies now render mirrored to the left
(sources -> policy -> selected group); source groups carry explicit
enabled state so they don't fall back to the incoming-edge check and
render dimmed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep architecture, layout/sort parity rules, changeset/deploy facts, and
the hard-won perf/camera gotchas; drop per-file trees and feature
narration discoverable from code. Fix stale claims (change types,
deploy order, provider names, focus-mode scope).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Peer/group/user-entered drafts used the networks-overview rhythm (160
base, policies 480/90) and scattered destinations across layout buckets.
Now frameless drafts use the live views' spacing (120 + DEFAULT_LAYOUT_
CONFIG), restack destinations as one x1000/100 column ordered like live
(groups first, then resources/peers by first policy edge), sort policies
with the views' sortBy, and name-sort destinations on group-view entry.
Existing destination resources get their real network stamped (no bogus
"No Network" badge), and the live destination-peer card sizes to content
(w-auto) like the draft peer card.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On client-side nav back to the page, nodes commit before fitView's
measurement wait completes and painted at the default viewport. The
first fit after mount now hides the viewport (cc-prefit, instant hide),
snap-fits without animation, and fades the scene in already framed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Live frames gain the draft chrome for real: Add Resource opens the
networks ResourceModalContent (real POST), routing bar Add + zero-state
open the real RoutingPeerModalContent; the Add band is reserved in both
modes
- Live frames contain destination resource-group rows like draft
(per-network ids); peer-only destination groups are skipped in the
networks view (live never drew them; networks-entered drafts drop them
+ their edges); draft group folding matches canDropGroupIntoNetwork
(one member resource in exactly one network, peers don't block)
- Identical geometry and order across modes: shared FRAME_GRID_BASE_X,
frame heights re-seeded after folding, group rows first, sources
(groups + source peers) as ONE 160-pitch column, policies at x480/90,
every column + the frame grid sorted by name
- Routing-peers dropdown polish: rows prefetch on trigger hover (no
skeleton flash), skeleton geometry matches the final list, exact list
height (no scrollbar for short lists), tighter search-to-rows gap
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
useGroupFocusDim and the focus drag-lock no longer gate on live mode;
className joins the history's transient keys so focus toggles never
create undo entries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Networks overview builds source -> policy node -> network frame with
smart edges (like draft) instead of labeled straight lines; sources
left, policies middle, packFrameGrid right; frames whose policies are
all disabled dim (frame + child rows)
- AnimatedLine is a SmartEdge-style bezier with a midpoint label chip
(orthogonal channel router removed); drill-out reveal zooms out
- Focus mode (live): clicking a group opens its panel, clicking a
peer (user view) or resource card sets focusedNodeId; everything off
the node's path dims to grayscale (forward+backward closures) via
cc-dimmed className; dimmed nodes are non-interactive and clicks fall
through to the pane which exits focus; node dragging disabled while
focused (dragging pans; per-node draggable:true removed - it
overrides the global flag)
- User-detail view removed: previousSelectedUser breadcrumb,
forceSinglePeerView/forceSingleUserView, user-peer history level,
onForceSingleUserView
- Bigger frame grid gaps (280/200)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fly-out's reveal reused the drill-in's grow-in (0.7x -> fit), which
read inverted going back; direction "out" now starts at 1.45x and
settles outward, mirroring the dive.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
React profiler showed ~2700-fiber User-Blocking commits (120-170ms) on
every drag tick, hover flip and click. Root causes and fixes:
- DraftModeProvider context value memoized (hover state changes handed a
fresh value to every useDraftMode consumer = every node)
- Every callback/object prop on <ReactFlow> is identity-stable via
useStableHandler; inline callbacks defeated GraphView's memo so each
nodes update re-rendered the entire canvas subtree
- selectedDestinationGroup split into its own context (group click
re-rendered every node via useCanvasUI)
- Edges subscribe to endpoint rects via useEdgeNodeRect (value equality)
instead of useInternalNode; ResourceNode's parent-hidden check likewise
- elevateEdgesOnSelect off (edge DOM reshuffle restarted dash animations)
- Draft persistence + history capture skip while dragging and pre-check
structurally before stringifying; history ignores transient node state
- Lazy router-rows fetch for draft frames too; Add Resource button uses
a ref instead of mounting useControlCenterData per frame
- Components panel, PeersToolbar, DestinationGroupPanel, CanvasToolbar,
DraftEmptyCanvas subscribe structurally (useStructuralNodes), not to
the churning canvas context
- Drag drop-target highlight throttled to ~8px steps
- Live networks overview arranged like draft (sources left, packFrameGrid
staggered frame grid right; d3 force layout removed from this view)
- Draft build: resource-only destination groups fold INTO their network
frame; frames seeded with the capped grid; useAnySourceGroupEnabled
skips when enabled is explicit
Verified via Chrome trace on the live app: 4ms/s JS during a 125Hz drag
over 36 network frames, no long tasks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
destinationResourceNode gets standalone: true in the shared builder —
resource destinations route to the StandaloneResourceNode card, peer
destinations get the same card surface as PeerNode's card variant
(previously both were transparent rows).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Draft mode (not drilled) shows the top-left network SelectDropdown like
live: "All Networks" first, canvas frames sorted by name, picking one
drills in. Drill history entries are consumed deferred (150ms) so the
transient deactivation during draft/live switches reuses the entry
instead of racing history.back() — browser Back after switching modes
now exits to the overview instead of leaving the page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
useDrillDownBrowserHistory tracks a stack of drill levels (network
drill-down, user-to-peer breadcrumb): entering pushes one same-URL
history entry, popstate exits through the same path as the UI back
arrow, UI exits consume the entry silently. SimpleConnection stroke
lightened from #595959 to #7d7d7d.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Warm SWR cache makes the view initialize before ReactFlow measured the
freshly remounted nodes; fitView now retries (bounded rAF loop) until
every target node has a measured size before fitting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getFirstGroup excluded All from the has-policies check, so an empty
non-All group could win over a populated All view. New order: non-All
group with policies, then All with policies, then any non-All, then any.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PeerNode now always renders the invisible AllHandles edge anchors
(GroupNode's pattern) so showHandles:false only hides the visible
connect bubbles instead of detaching edges.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No more waiting on the SWR /policies revalidation (or a layout reset):
refreshLiveView rebuilds the current view with the saved policy spliced
in, keeping positions and camera; all view appliers accept a
policiesOverride and the policy modal reads the canvas node's policy
first in both modes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
utils/canvas-transition.ts: one continuous zoom stitched across an invisible
swap — accelerating dive/zoom-out + fade-out (half ease-in), swap while
transparent, grow-in reveal + fade-in (half ease-out) into a viewport
computed from the new scene. One-liner facades drillInto/drillOutOf (Node or
Rect targets) for any future drill-down; isCanvasTransitionActive suppresses
view-init fitViews mid-transition.
- Draft drill-down enter/exit refactored onto it (position-aware close-up on
exit, exitingRef guards the reconciling repair; frame layout + edge
attachment + row/card rendering key off frame.hidden so nothing morphs
while visible)
- Live network select dives into the clicked frame (dropdown picks zoom from
center), back flies out to the overview
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backed out of the drill inside draft → live lands on the all-networks
overview instead of the stale drilled snapshot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- utils/drilled-layout.ts: ONE layout (applyDrilledLayout, 95px resource
pitch, getDrilledFrameAnchor incl. the column's +5 centerY offset) shared
by the live single-network view, the draft drill-down, and live→draft
- Draft drill re-lays the kept world out with it (parent positions
snapshotted, restored on exit with hidden flags + viewport); drilled frame
grid is a fixed-pitch single column, cards auto-size (no forced width)
- Live networks render as read-only frames (RoutingPeersBar without Add,
frame anchors/handles for edges, hover highlight, click/row-click drills,
bottom padding instead of the draft Add band); drilled resources render as
standalone cards in live too, with direct policy→resource edges
- Entering draft from live drilled carries the network as a frame and enters
the draft drill-down in the same commit (pre-hidden, no flash); exiting
draft while drilled lands in the live view of that network
- Live all-networks lines are blue with an All fallback label; peer sources
drawn in both live network views; NEW badge only for id-less networks
- Drill header: name chip matches the live selector (icon size/color,
typography, paddings), edit segment for all frames (existing networks open
the real network modal), RoutingPeersBar height/chevron aligned with the
fixed-height network selector; SelectDropdown defers onChange past its
close animation (no blink) and uses a 16px chevron
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extracts the frame's [status | Add] routing button group + searchable
routers popover into RoutingPeersBar (indicator, row type, peer-count and
sort helpers). NetworkNode consumes it unchanged; the live single-network
header's NetworkRoutingPeerCount now renders the same bar over the API
routers, with Add/edit navigating to the routing-peers tab. Status button
left padding 14px.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Matches the context menu's Add Resource (addResourceToFrame); the editor
still opens on row click.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Frame status button gains a ChevronsUpDown trigger and a PeerSelector-style
popover (search by peer/group name, no enter badge): one row per router with
muted OS icon + peer name or group badge + group name (peer count inline)
- Draft router rows open the routing-peer modal prefilled (editChangeId on
routingPeerModal); save replaces the create-router change
- Status counts peers, not routers: groups contribute their peer count,
disabled routers contribute nothing and render at 50% opacity, sorted last
- Floating bar nudged 2px left; NetworkRouter API routers of existing
networks are listed too (read-only)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Drag a group onto a network frame → converts in place to a resource-group
row inside it (same node id, policy edges re-attach to the frame). Eligible
when the group is empty, has a resource already in that network, or carries
unassigned draft resources (canDropGroupIntoNetwork, tested)
- Grouped standalone draft resources ride on the group node (data.draftResources);
dropping the group into a frame records create-resource changes assigning
them to that network with the group membership
- Frame children are selectable:false — full-width rows no longer join any
grazing rubber-band selection (phantom Create Group members)
- Create Group resolves draft resources via getDraftResource so their new-…
ids reach the group (count label, changeset membership, policy regrouping)
- Group count label leads with resources: "2 Resources, 1 Peer"
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dragging from a resource onto a peer/group prefills the create-policy modal
with the resource as destination and the peer/group as source (framed
resources keep the network destination scope). Also: context-menu Add
Routing Peer uses Share2Icon, NetworkNode Add buttons use CirclePlusIcon.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shared getTopZIndex (max settled z + 2, beating frames and their children)
applied in placeNode, dropExistingNetworkFrame, addNewGroup, and
onNodeDragStop — e.g. a peer dropped over a network frame paints above it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Removing an existing policy records an update-policy change with emptied sources/destinations (draft policies still just cancel their pending create)
- Components panel re-drop (and network/resource auto-drawn policies) prefer the pending update-policy state over API data, so a disconnected policy doesn't come back with its deployed sides
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Dropping an existing network/resource also draws the API policies targeting its resources (getPoliciesTargetingResources + drawResourcePolicies)
- Policy context menu Remove (CircleMinus, no confirm) removes only the policy node and edges, keeps sources/destinations on canvas, cancels pending create / drops pending updates
- Deletes (policy/resource/group) confirm: marked for deletion, deleted on Review & Deploy
- Group node removal strips the group from connected policies (like peers/resources); deferred policy sweep skips policies removed in the same cascade
- Remove icons switched to CircleMinusIcon; unit tests for the policy matcher
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The context menu's item-click and outside-click shared one onClose that
cleared the panel too, so "Details" opened the group panel then instantly
closed it. Split into onClose (menu only, for item clicks — stopPropagation so
it doesn't reach the document listener) and onDismiss (everything, for outside
click/scroll). Details now opens the panel and it sticks; outside click still
dismisses menu + panel in one click.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
centerNodesVertically is shared by the live hierarchical layout and the draft
auto-arrange; the height-aware pitch added for tall network frames broke live
column alignment (unequal per-node pitches). Make height-aware pitch opt-in
(auto-arrange only); live keeps a fixed pitch so equal-height nodes stay in a
straight line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Dropped resources are standalone cards with an inline network label / "No
Network" picker (DraftResourceNetworkModal); assign via drag-into-frame or
the picker; extract StandaloneResourceNode
- Existing networks drop as full frames (dropExistingNetworkFrame) with their
resources as children; reparent an already-dropped resource instead of
duplicating its node id
- Refactor frame-ness to an explicit data.frame flag (isFrameNode /
isDraftNetworkNode); network-new- now means only "draft network", existing
frames keep their real id
- Resource context menu: Edit (all), Enable/Disable, Rename (draft), and
Remove vs Delete (existing framed) — wired into the changeset via new
update-resource / delete-resource change types + deploy PUT/DELETE; draft
enabled folds into create-resource
- Components panel: existing resources show "name - network" and can't drop
into a frame; resource drags highlight the target frame (white border)
- Frame z-layer so no node renders between a frame and its resources
- Auto-arrange stacks columns by measured node height
- Group node "Details" menu item opens the panel; policy left-click no longer
leaves a halo; misc spacing/badge/icon tweaks
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add "Add Resource Group" context-menu item (default group icon); blank
group rows drop first in the frame and count toward layout/overflow
- Group naming now consistent ("Group", "Group (2)", …) everywhere
- Resource groups: Rename + Details context menu, sky halo on right-click,
hover-only white ring during connects, "No Resources" count label
- Group node context menu gains "Details" (list icon) to open the panel;
clicking a group no longer toggles the panel closed (only outside click)
- Newly added resources/groups always land first in the frame grid
- Auto-arrange stacks columns by measured node height so tall network
frames (with floating routing bars) don't overlap neighbours
- Tune Add Resource button spacing (top gap + bottom padding)
- Remove routing-peer HelpCircle + HA tooltip; nudge "+N more" cell 1px
- Fix User Device select node NEW badge alignment
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- PeerNode: server/agent placeholder Install button uses the shared Button
(secondary, xs) to match the User Device select node.
- NetworkNode: rework the frame's floating controls — an "Add" resource
control and a routing-peers button group ([â—Ź status | + Add]) sized to
match the Install button (secondary, xs).
- Add a secondary "Add Resource" button in the frame body: centered when
empty, full-width bottom row once resources exist; opens the resource
editor. Reserve a bottom band for it in the frame layout (mutually
exclusive with the "+N More" overflow footer).
- Sparse frames (0/1 resource) match the two-resource width; empty frames
get a little extra height so the centered button breathes.
- Hovering the floating controls / add button no longer highlights the
frame or reveals its ConnectHandle (new ConnectHandle `hidden` prop).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the draft network frame's resource affordances:
- Move adding into a header "Add" dropdown (chevron) on the right of the
name/count, with Routing Peer (server icon) and Resource (resource
icon) items. Removes the separate "Add" button from the floating
routing-peer status pill and drops the always-present "Add Resource"
child row (AddNetworkResourceNode deleted).
- Summarize resources past the visible cap with a "+N More" footer
rendered by NetworkNode in a band useNetworkFrameLayout reserves at the
frame bottom (pointer-events-none, so clicking still drills in),
replacing the fade mask and the short-lived ResourceOverflowNode.
useNetworkFrameLayout is simplified to lay out only resource children and
sweeps legacy add-resource/overflow child nodes from persisted drafts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New draft networks are created without seeded resources: drop the
layout-testing seed (30 fake resources + 10 resource groups) from
addDraftNetwork so a network starts empty.
Every network frame — including a freshly-added empty one — now always
contains the "Add Resource" child row: useNetworkFrameLayout creates it
if missing and no longer removes it when the frame is empty (dead
obsoleteAddRows path removed). The centered "No resources yet" hint is
gone; the header subtitle reads "No Resources" when empty and the frame
keeps its header separator.
Also removes the bottom-fade gradient overlay from the existing-network
card's resource preview grid.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Resource editor gains a create-mode (frame's Add Resource row opens the
modal; the node is only created on save) and the provider nesting fix
for the resource-modal refactor; new DraftNetworkEditModal (name +
description) shared by the network context menu and drill-down header.
- Context menus: Edit items (SquarePen) for policies/networks/resources,
policy Remove vs Delete split (draft vs API), PencilLine renames,
Add Resource on frames, Policy above Group ordering.
- Components panel: draft peers/policies/networks listed with NEW badges
(IP placeholder, kind icons), "Networks & Resources" category with
existing networks droppable as live cards, virtualization measurement
fixes, panel hides once a real drag starts.
- Selection toolbar: Remove option next to Create Group, padding fix.
- Auto-names drop the "New " prefix ("Group", "Resource").
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spec §10 implementation plus the frame interaction model:
- Connections: peer/group drops onto a frame or its contained rows open
the create-policy modal (source prefilled, destination scoped to the
network, one-way); POLICY↔network drags open the minimal destination
picker (DraftNetworkDestinationModal). Drag no longer creates routers.
- Policy edges to framed resources attach to the frame in the parent view
and to the actual resource in the drill-down (useFrameEdgeAttachment,
pure logic + tests in utils/frame-view).
- Drill-down: clicking a frame enters a live-mode-like single-network view
(no frame box, square-ish grid, back/name/edit/routing-count header);
hidden flags + viewport restore on exit.
- Routing peers: the frame button group and context menu open the networks
page's routing-peer modal; routers are changeset-only (no canvas nodes),
counts derive from create-router changes, deploy sends the modal's
metric/masquerade/enabled.
- Frame layout: 4x2 row-major grid capped at 6 visible resources with a
fade-out on the last row, always-visible Add Resource row, tunable
padding/gap/width constants, drop z-order fix for stacked frames.
- Node visuals: NEW badges after draft names, frame-hover highlights
header/border/icon boxes, placeholder select copy, min widths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- NetworkModal, NetworkRoutingPeerModal and NetworkResourceModal gain a
useSave={false} mode: no API calls, values return via onSaved — the
draft canvas reuses the exact networks-page modals.
- AccessControlModal: destinationScope restricts the destination selector
to a network's resources/groups (no peers) and locks the policy one-way.
- PeerGroupSelector: groupIds whitelist (disables inline group creation),
initialTab (open tab without reordering triggers), scoped empty states,
and a default icon for type-less draft resources.
- ResourceBadge/PeerOperatingSystemIcon: default resource icon and
Server/Agent/User-Device icons for draft placeholder peers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NetworkNode: RoutingPeersIndicator and AddRoutingPeerButton extracted
(shared by the draft frame and the live card). The invisible
whole-node target handle becomes handles/FullAreaTargetHandle, reused
by AllHandles, PolicyNode, and NetworkNode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Frame children are draggable again (non-draggable nodes emit no drag
events — the gesture fell through to canvas panning). A drag-start
handler snapshots child + frame positions; each drag tick transfers
the child's displacement to the frame while snapping the child back
into its slot, so the whole frame (and every resource in it) follows
the cursor. Framed resources stay excluded from drag-into-group.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restore ReactFlow's fall-through: frame children are non-draggable,
so a drag on a resource moves the parent frame (and everything in
it). Clicks still open the resource editor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dragging a non-draggable frame child fell through to dragging the
parent — contained resources now carry the "nodrag" class (clicks
still open the editor). Frame width reduced to 340px.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
specs/limitations.md: framed resources can't drag into groups (use the
editor's Groups field), routing-edge-only deletion leaves its router
change, network descriptions not editable on canvas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
420px wide with a taller header (64px), roomier rows and padding;
contained resources keep spanning the frame width.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Contained resources are laid out by the frame: not draggable, and
spanning (basically) the full frame width; popping out of a removed
frame restores free dragging and auto width
- Network frame widened (360px) with slightly taller header/padding
and peer-style hover treatment
- Group nodes get a little more padding
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- No "Set up" affordance above incomplete resources — the dimmed
x.x.x.x placeholder and click-to-edit carry that state
- The resource editor no longer opens on drop (click / context-menu
Edit instead)
- Network frames: solid background with a dashed border
- Routing-peer indicator hidden entirely at 0 (the floating Add
Routing Peer button covers it); NEW badges removed from draft
resource and network nodes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Dropping a resource auto-creates a draft network FRAME around it —
a bordered, slightly opaque container node; the resource lives
inside as a ReactFlow child (frame grows per member, children pop
out at absolute positions when the frame is removed)
- Draft networks (panel template + editor inline-create) use the
frame rendering: header with name/NEW badge/routing traffic light,
"No resources yet" hint, floating Add Routing Peer button
- The draft resource node mirrors the placeholder-peer card: derived
type icon box, NEW badge, dimmed "x.x.x.x" address placeholder;
left-click and context-menu Edit open the resource editor
- Resources carry a single LEFT connect handle — dragging into a
policy sets it as the destination (source side rejected); network
assignment reparents into frames instead of membership edges
- Auto-arrange is parent-aware: child connectivity resolves onto the
frame, children keep their relative positions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements specs/draft-networks-resources.md (v1):
- Networks drop with a create-network change (unique auto-name,
deployable immediately); NetworkNode is draft-aware — NEW badge,
draft resource count/previews, routing-peer traffic light from
routing edges, floating "Add Routing Peer" button at 0 routers,
Rename / Add Routing Peer / Remove context menu (removal cascades
to dependent resource/router changes)
- Resources drop + open the new DraftResourceEditorModal (unique
name, validated address, network selector with API + draft
networks and inline create, optional groups/description);
incomplete resources carry an amber "Set up" affordance and stay
out of the changeset; completing one re-runs referencing policies
- Routing peers: peer/group → network connects (and the Add Routing
Peer install flow) draw "routes" edges and record create-router;
placeholder routers gate until install, then the upgrade sweep
records them with the real peer id
- Resources are destinations-only (hard-rejected as connect
sources); draft resources participate in policies with new-… ids
via additionalResources, join groups (deploy fans membership out
through the resource's own groups field)
- Deploy: groups → networks → resources → routers → policies with
networkClientToId / resourceClientToId resolution and live-modal
router defaults; Review & Deploy shows non-blocking warnings for
router-less networks and policy-less resources
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User Device placeholder:
- Drops a canvas node like Server/Agent, rendered as a live-style
peer-select dropdown ("Select user device..."); choosing a peer
upgrades the node in place and keeps the dropdown so the selection
can be switched — policies and edges follow the new peer
- Install buttons float top-left above all placeholder nodes (inside
the node so they zoom with the canvas), secondary styling
- Placeholder ids renamed new-<uuid> → draft-<uuid>
Group membership follow-through:
- Grouping/dragging placeholders records them as members with their
draft ids; on install/select the id is renamed across the changeset
(replacePeerIdInGroups) and group nodes' addedMembers; deploy
filters never-installed draft ids
- Group counts/addedMembers update on every canvas instance of a
group (source node + destination copies)
- Policies referencing a grouped or removed peer/resource are rewired
(group takes the side) or cleared (side emptied) — an incomplete
policy drops its pending create/update so broken state never deploys
Other:
- Peer view → draft: the picked peer is skipped when a canvas group
already contains it (was overlapping the group node)
- Auto-arrange vertical spacing increased (160/80/160)
- Connect rules extracted to utils/draft-connect.ts and capability
predicates to utils/node-capabilities.ts (pure, unit-testable),
wired into NodeContextMenu + useDragToGroup
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Components panel & context menus:
- Panel not-found state matches the global search modal styling
- Context menus (canvas + node) flip away from viewport edges
- User Device template delays panel close so its modal opens first
- Template icon boxes match existing entities (solid bg)
Placeholder peers (Server / Agent):
- Node mirrors the real peer card (layout, text sizes, truncation);
IP slot shows a dimmed placeholder from the account network range
- Unique per-drop names ("Agent", "Agent (1)", ...), renamable via
context menu with uniqueness check across draft peers
- Selectable in policy modals (additionalPeers in PeerGroupSelector)
and connectable like real peers via their draft-<uuid> ids
- Install modal suggests a unique hostname (netbird up --hostname),
stamped on the node; useDraftPeerUpgrade watches the peers list and
swaps installed placeholders for real peers in place, re-recording
their policies into the changeset
Policies in draft:
- Policy template drops a blank policy node (no modal) named
"Policy" / "Policy (1)"; blank/undeployable policies stay out of
the changeset until they have both sides and no placeholder refs
- Peers can be dragged onto policy sides (single-peer XOR groups)
- Connect prefills the create modal (peers as peer resource, groups
as lists) and resets stale prefill state on every connect
- Peer selector dropdown opens on the tab of the current selection
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migrate peer/group/user/network view policy edges from the DirectionIn
edge type to SmartEdge, enabling bidirectional rendering and dynamic
side selection in live mode. DirectionIn and BidirectionalEdges remain
registered but unused.
- Persist draft changes and canvas to localStorage, restored on mount
- Snapshot-based undo/redo for canvas and changeset (DraftHistoryContext)
- Review & deploy modal with grouped changes and deploy changeset hook
- Discard draft flow and leave guard for unsaved drafts
- Group rename modal and draft node creation hooks
- Move components panel into draft/ and adjust related UI
In live mode on the networks tab, the header network selector now only
renders when there is at least one network, matching the empty state
shown by ControlCenterEmptyStates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Overlays feature/control-center-edit's control-center paths onto main:
new draft/ mode with canvas toolbar, keyboard shortcuts, drag-to-group,
create-group modal, connect-handles for drawing policies, SmartEdge
routing, and refactored ControlCenterContext into CanvasState + UI +
Policy providers. Adds hooks/views split (peer/group/user/network).
AccessControlModal + useAccessControl gain initialSourceGroups /
initialSourceResource so draft-mode policy creation can seed the source
side (previously only destination could be pre-filled).
Unrelated cloud-repo changes (data-testid -> data-cy renames, DNS,
reverse-proxy, integrations, etc.) are excluded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-06 17:10:19 +02:00
242 changed files with 35299 additions and 7744 deletions
| Control Center | `control-center-draft.spec.ts`, `control-center-draft-connections.spec.ts`, `control-center-draft-matrix.spec.ts` | `@control-center` |
Loaded 20 of 242 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.