Skip to content

feat(fleet): saved server lists, with CRUD - #22

Merged
ralyodio merged 1 commit into
mainfrom
feat/fleet-lists
Aug 30, 2026
Merged

feat(fleet): saved server lists, with CRUD#22
ralyodio merged 1 commit into
mainfrom
feat/fleet-lists

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

A tag says what a server is. A list is a set you assembled by hand and want back — "the four boxes behind the EU load balancer" is not a property of any one of them, and re-ticking it every time is the friction this removes.

CLI

diskpush fleet lists save eu-edge --on 'web-*,cache-01' --description "behind the EU LB"
diskpush fleet lists                       # read
diskpush fleet lists show eu-edge
diskpush fleet lists rename eu-edge edge
diskpush fleet lists remove edge           # servers untouched

diskpush fleet run "uptime" --on list:eu-edge
diskpush fleet upgrade --on 'list:eu-edge,!web-03' --sudo

Desktop

Lists are chips at the top of the server sidebar, above the tags. Clicking one ticks exactly its members; ticking servers by hand offers Save these 3; the × on a chip deletes the list and leaves the servers alone.

The decisions worth reviewing

  • list: is a prefix, so a list and a server may share a name without either shadowing the other.
  • A list stores each member's connection id and the name it had when saved. The id resolves, so a renamed server stays in the list; the name keeps the list readable afterwards.
  • A deleted member is named and refused, not silently skipped — the same rule a selector term already follows. A list that quietly got smaller is how a command misses the one server it most needed to reach.
  • Saving resolves the selector now and stores the result. A list is a set someone chose, not a query that might mean something different next week; a tag is what you want for dynamic behaviour.
  • Picking a list replaces the selection rather than adding to it. Picking a list is saying "these"; a union with whatever was already ticked would run on servers nobody chose.

Bug found while building it

--on 'all,!list:local' excluded nothing. parseSelector is what splits a comma-separated --on, so expanding list: terms before that left all,!list:local as one unrecognised string and the exclusion silently did nothing — and fleet servers bypassed expansion altogether. Both now go through one resolver, in one order.

Verified

Against a real store, end to end: create from a selector, read, show (with a live/MISSING column per member), update by saving over it, rename, delete, use as a target, exclude with !list:, an unknown list refused, and a deleted member named rather than dropped.

The desktop side was screenshotted rather than reasoned about: chips above the tags, clicking eu-edge ticking exactly its three members with the run button following to Run on 3 servers, and the inline save field. No CSP violations.

507 tests, up from 498. Typecheck and pnpm smoke:desktop green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UeSWg1Czsb2Lwxj8vHUnA4

A tag says what a server *is*. A list is a set someone assembled by hand and
wants back — "the four boxes behind the EU load balancer" is not a property of
any one of them, and re-ticking it every time is the friction this removes.

CLI:

    diskpush fleet lists save eu-edge --on 'web-*,cache-01' --description "..."
    diskpush fleet lists                       # read
    diskpush fleet lists show eu-edge
    diskpush fleet lists rename eu-edge edge
    diskpush fleet lists remove edge
    diskpush fleet run "uptime" --on list:eu-edge

Desktop: the lists are chips at the top of the server sidebar, above the tags.
Clicking one ticks exactly its members; ticking servers by hand offers "Save
these 3"; the x on a chip deletes the list and leaves the servers alone.

The decisions worth knowing:

  - `list:` is a prefix, so a list and a server may share a name without
    either shadowing the other.
  - A list stores each member's connection **id** and the **name it had when
    saved**. The id resolves, so a renamed server stays in the list; the name
    keeps the list readable afterwards.
  - A member whose connection has been deleted is **named and refused**, not
    silently skipped — the same rule a selector term already follows. A list
    that quietly got smaller is how a command misses the one server it most
    needed to reach.
  - Saving resolves the selector *now* and stores the result. A list is a set
    someone chose, not a query that might mean something different next week;
    a tag is what you want for the dynamic behaviour.
  - Picking a list in the desktop *replaces* the selection rather than adding
    to it. Picking a list is saying "these", and a union with whatever was
    already ticked would run on servers nobody chose.

Fixed while building it: `--on 'all,!list:local'` excluded nothing.
`parseSelector` is what splits a comma-separated `--on`, so expanding `list:`
terms before that left `all,!list:local` as one unrecognised string and the
exclusion silently did nothing — and `fleet servers` bypassed expansion
altogether. Both now go through one resolver, in one order.

Verified against a real store end to end: create from a selector, read, show
with a live/MISSING column per member, update by saving over it, rename,
delete, use as a target, exclude with `!list:`, an unknown list refused, and a
deleted member named rather than dropped. The desktop side was screenshotted:
chips above the tags, clicking `eu-edge` ticking exactly its three members and
the run button following to "Run on 3 servers", and the inline save field.

507 tests, up from 498.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UeSWg1Czsb2Lwxj8vHUnA4
@ralyodio
ralyodio merged commit 61e2098 into main Aug 30, 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