Skip to content

Flatten server hierarchy into groups, add notes and tags#164

Open
passcod wants to merge 8 commits into
mainfrom
pr/server-groups-and-tags
Open

Flatten server hierarchy into groups, add notes and tags#164
passcod wants to merge 8 commits into
mainfrom
pr/server-groups-and-tags

Conversation

@passcod
Copy link
Copy Markdown
Member

@passcod passcod commented May 22, 2026

  • Replaces the servers.parent_server_id tree with a first-class server_groups table; each server has a nullable group_id. Incidents rekey from server_id to server_group_id so the schema matches what code was already enforcing.
  • Both servers and groups gain a freeform notes text field and a string→string tags jsonb map (new commons_types::server::TagMap).
  • New public-server GET /tags endpoint returns the merged tags for the calling device's server — the group's map overlaid by the server's, server wins on collision, ungrouped returns just the server's tags.
  • Ungrouped servers still record issues but skip the incident flow; assigning a server to a group runs a catch-up re-evaluation over its open issues so anything that warrants an incident promotes retroactively.
  • React UI rebuilt around groups: /servers opens to a Groups list with a sibling Ungrouped tab, new /groups/:id detail + /groups/:id/edit routes, a reusable TagsEditor (key/value rows with dedup), and a ServerNameWithGroup helper that prefixes server names in lists/headers with Group · Server (interpunct). The Status page now renders one card per group, bucketed by the highest-ranked member.
  • Removes the CanopyTicket.central_public_key field and its parent-lookup in upsert_from_ticket; imported servers start ungrouped and operators assign a group from the admin UI.

passcod added 8 commits May 22, 2026 18:18
The server tree (parent_server_id self-reference) is replaced by a
first-class `server_groups` table; each server has a nullable `group_id`
pointing at its group. Both servers and groups gain freeform `notes` text
and a string→string `tags` map (new `commons_types::server::TagMap`).

Incidents rekey from `server_id` to `server_group_id`. NewEvent::save
records the issue normally for ungrouped servers but skips the incident
flow until a group is assigned; Server::assign_to_group then runs
re_evaluate_incident_membership across the server's open issues so the
catch-up promotion happens automatically. central_public_key is removed
from CanopyTicket (it only fed the old parent-link logic).
Adds the private-server `server_groups` module (CRUD + search) and a
public-server `GET /tags` endpoint returning the merged server+group
tags for the authenticated device. Drops `list_roots`/`search_parent`
and the parent-server-derived child fan-out from the private servers
handlers; `get_detail` now surfaces the server's group and siblings;
`statuses` exposes `group_details` (replacing `server_details`) and
buckets groups in `server_grouped_ids` by their highest-ranked member.

Tests updated for the new shapes: parent_server_id / search_parent
cases become group_id / notes / tags cases, the incidents test seeds a
group, and new tests cover ungrouped-then-grouped issue promotion plus
the public tags endpoint will follow in a separate change.
Helpers that produce a server for incident tests now also create a
group and link the server to it, so the event flow can promote
issues to incidents. Queries against incidents.server_id are
rewritten to join via the server's group_id. Updated tests track
the API rename of statuses.server_details → group_details and the
shape change from CentralServerCard → ServerGroupCard. The nil-server
test now verifies events on the meta server *don't* open incidents,
since the nil server is intentionally ungrouped.
…d status page

The /servers page now opens to a flat list of groups with a sibling
"Ungrouped" tab for servers not yet placed in a group. New routes
/groups/:id and /groups/:id/edit show and mutate the group's name,
notes, and tag map (via a new key-value TagsEditor). ServerEdit gains
a group picker, a notes textarea, and the same tags editor; the old
parent-server autocomplete is gone.

The status page (/) now renders one card per group: each card shows
the group name, a row of StatusDots — one per member server — and
the version pulled from the most-recently-pushing member. Buckets
along the page are keyed by the highest rank of any member.

Incidents/issues and the IncidentCard / IssueRow components track
the rekey: filter rows now key on server_group_id, the issue list
prefixes each row with the group's name via the new
ServerNameWithGroup helper, and IncidentDetail/IncidentCard label by
the group rather than reassembling a server label.

Issue data picks up `server_group_name` (off a new
`Server::group_names_by_server_ids` batch lookup) so the row prefix
doesn't need an extra round-trip.
…pecs

resetSeededTables now wipes server_groups too. New seedServerGroup
helper inserts a row with name/notes/tags. seedServer accepts a
groupId (string | null) and notes/tags fields; parentServerId is
gone. The servers.spec.ts page tests now check the Groups +
Ungrouped tabs and reach the seeded group via /groups/<id>. The
status.spec.ts test seeds a group per rank and verifies the new
card link points at /groups/<id>. groups.spec.ts is new: it covers
detail (name, notes, tags, members), the empty-group banner, and
the edit-form prefill.
…on path

tests/public-server/tags.rs covers the four cases of GET /tags:
group-only tags propagate, server tags overlay (winning on key
collisions while non-colliding group keys carry through), an
ungrouped server returns just its own map, and a device without an
attached server gets a 412 (matching the events/* convention).

Also fixes two issues found while exercising the e2e suite:

- DeleteOutline → DeleteOutlined. The @mui/icons-material package
  ships the outlined variant under the …Outlined suffix; without
  the correct import the private-server build.rs's embedded-frontend
  step couldn't compile and `just test-e2e` silently ran against a
  stale binary, masking many failures.
- Two e2e selectors needed widening: the group-edit Name field is
  `required` so MUI appends a `*` to the label (regex matches from
  the start, not the end), and the server-detail h1 now contains
  `<group> · <server>` so the role matcher scopes by a name regex
  to skip the surrounding "Canopy" app-bar h1.
Plan delivered:
- server_groups + nullable servers.group_id, with notes/tags maps on both,
  incidents rekeyed to server_group_id, server kind decoupled from hierarchy;
- ungrouped servers can record issues but don't open incidents; assigning a
  group re-evaluates membership and promotes any pending issues retroactively;
- new public GET /tags endpoint merging the group's and server's tags;
- private API server_groups CRUD + search + statuses.group_details + the
  Status page bucketed by the highest-ranked group member;
- React UI: Groups + Ungrouped tabs, group detail/edit, TagsEditor,
  ServerNameWithGroup, IncidentDetail/IssueRow rebadged for groups;
- Rust + e2e suites green.
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