Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions api/internal/auth/perms.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func (u *User) Can(perm string, namespaced bool, cluster, ns string) bool {
}

// CanDiscoverCluster permits selecting a cluster without granting node inventory
// access. A server reader in one namespace still needs to find that cluster.
// access. Server and backup-destination readers still need to find their cluster.
func (u *User) CanDiscoverCluster(cluster string) bool {
if u == nil {
return false
Expand All @@ -171,9 +171,14 @@ func (u *User) CanDiscoverCluster(cluster string) bool {
if u.Can("cluster:read", true, cluster, "") || u.Can("modules:read", true, cluster, "") || u.Can("modules:manage", true, cluster, "") {
return true
}
// Install-time storage is now displayed in the local cluster's configuration.
// A central configuration grant must not reveal remote registrations.
if cluster == scope.DefaultCluster && (u.Can("config:read", false, "", "") || u.Can("config:manage", false, "", "")) {
return true
}
for _, ck := range []string{cluster, "*"} {
for _, perms := range u.Perms[ck] {
if permSetHas(perms, "*") || permSetHas(perms, "servers:read") {
if permSetHas(perms, "*") || permSetHas(perms, "servers:read") || permSetHas(perms, "destinations:read") || permSetHas(perms, "destinations:manage") {
return true
}
}
Expand Down
5 changes: 5 additions & 0 deletions api/internal/handlers/clusters_discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ func TestClusterDiscovery_FiltersRegistrationsAndInventoryCapability(t *testing.
{"local reader", "local", "*", "cluster:read", []string{"local"}, true},
{"remote reader", "remote", "*", "cluster:read", []string{"remote"}, true},
{"remote server operator", "remote", "gameplane-games", "servers:read", []string{"remote"}, false},
{"remote destination reader", "remote", "gameplane-games", "destinations:read", []string{"remote"}, false},
{"remote destination manager", "remote", "gameplane-games", "destinations:manage", []string{"remote"}, false},
{"wildcard destination reader", "*", "gameplane-games", "destinations:read", []string{"local", "hidden", "remote"}, false},
{"local configuration reader", "local", "*", "config:read", []string{"local"}, false},
{"panel configuration manager", "remote", "*", "config:manage", []string{"local"}, false},
{"namespace admin", "remote", "gameplane-games", "*", []string{"remote"}, false},
{"unrelated permission", "remote", "gameplane-games", "backups:read", nil, false},
{"namespace inventory is insufficient", "remote", "gameplane-games", "cluster:read", nil, false},
Expand Down
49 changes: 49 additions & 0 deletions assets/design-export/MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -2825,3 +2825,52 @@ Source: `web/src/components/backups/BackupDetailDrawer.tsx`. The recorded state
### Follow-up alignment — 2026-11-04

Re-exported `zhLZN` and `bHJy6` from Pencil after reviewing the CI captures. The drawer now explicitly removes HeroUI body/footer margins and uses foreground text for values, matching the design's 20px content inset. The recorded reference is 400px wide (440px drawer minus two 20px insets); native Windows scrollbars consume additional space in the live scroll area. Coverage cards remain 98px tall. Header/footer heights, field line heights, the phase chip, and action-button typography now match the component styles. Both nodes were visually inspected in Pencil and the recorded state was inspected in Chrome; no comparison thresholds were changed. JSON and 2× PNG exports use the same methods above.


## Incremental export 2026-11-09 — Clusters and all-cluster Nodes navigation

The canonical infrastructure navigation is **Clusters** plus **Nodes**. Nodes lists inventory across authorized clusters with search, cluster/status filters, sortable columns, explicit partial coverage, and unknown metric values. Cluster details reuse that inventory table with a fixed cluster scope and separate Overview, Nodes, and Configuration tabs.

| Node | Name | Change |
| --- | --- | --- |
| `BdmAW` | Screen/Nodes — All clusters | New desktop inventory: readiness and reporting totals, CPU/memory coverage, filters, partial-inventory notice, shared sortable table, and Add node. |
| `I38x1` | Gameplane/Node Inventory Table | New reusable table composed from existing HeroUI column/cell primitives. Columns: Node, Cluster, Status, CPU, Memory, Pods, Uptime. |
| `wqnM9` | Screen/Cluster Details — Overview | New registration identity, API connectivity, node readiness, Kubernetes version, and workload/inventory links. |
| `J8GzM9` | Screen/Cluster Details — Nodes | New fixed-cluster variant of the same inventory table, with search/status filtering and View all nodes. |
| `OecJ5` | Screen/Cluster Details — Configuration — Local | New local access/download, read-only storage class, and scoped backup destinations. Built-in local registration cannot be removed. |
| `T70dJ` | Screen/Cluster Details — Configuration — Default storage | New empty-storage-class variant; new volumes use the cluster default. |
| `t56aLu` | Screen/Cluster Details — Configuration — Remote | New remote-managed access/storage state, scoped backup destinations, and separated registration-removal section. |
| `IBUqJ` | Screen/Nodes — Add node target | New explicit target-cluster selection before enrollment. Remote management and local operations availability remain capability dependent. |
| `N2nTf` | Screen/Mobile — Nodes — All clusters | New 390px-wide stacked rows with search, cluster/status filters, sorting, readiness, and partial coverage. |
| `kKFX9` | Gameplane/App Sidebar | Added Nodes (`VqyiS`) after Clusters. |
| `Mp7Ep` | Gameplane/Admin Settings Nav | Removed Backup destinations; it now belongs to cluster Configuration. |
| `iYUWc`, `WrmU5` | Registered cluster lists | Open cluster plus View nodes entry points; registration removal is in detail Configuration. |
| `Y96CaE` | Gameplane/Cluster Resources Card | View nodes opens `/nodes` while preserving the Dashboard cluster filter. Canonical dashboard instances updated. |
| `FyV6E`, `rCQaM` | Shared fleet filters | Cluster and All clusters terminology. |

The previous mixed Cluster Settings screens (`j9W8A`, `dxdEi`), Admin backup-destination screens (`RC3Kf`, `D9QYZ`), standalone inventory-selection screen (`Be7ZW`), and list-based removal screen (`HVhmb`) are retained under **Legacy/** names with explicit supersession context. Their old storage and form states remain available as historical reference.

Changed 51 product-copy entries from location/locations to cluster/clusters across fleet screens, wizard selection, central-management explanations, and reusable filters. Technical identifiers and URLs were not renamed.

**Export coverage:** 54 objects have refreshed `json/<id>.json` and 2× `screenshots/<id>.png`:

`kKFX9`, `Mp7Ep`, `I38x1`, `BdmAW`, `wqnM9`, `J8GzM9`, `OecJ5`, `t56aLu`, `IBUqJ`, `N2nTf`, `iYUWc`, `WrmU5`, `j9W8A`, `dxdEi`, `RC3Kf`, `D9QYZ`, `Be7ZW`, `HVhmb`, `T70dJ`, `j24cXg`, `W8idqY`, `nNL3E`, `vUqMl`, `f1Vga`, `UMJli`, `kK8Ji`, `DPrYX`, `fK8Bi`, `tTSdi`, `uMiwd`, `Wj0V4`, `zFiOW`, `F9pUrx`, `oyoTs`, `QQtUD`, `UCNBm`, `pnAbF`, `Cb3eC`, `fbBqA`, `YPKbV`, `NKiqf`, `zj5a3`, `GR1G9`, `n2qdi`, `RpHBg`, `l64bd`, `Uzzje`, `FOVMf`, `r139UP`, `w34N9`, `FyV6E`, `rCQaM`, `NLu2I`, `Y96CaE`.

**Method and verification:** Edited only through Pencil MCP. JSON came from `Get` at depth 9 with targeted expansion of elided child trees; exports retain component references, with referenced changed components exported separately. PNG came from `Export(..., "png", ..., {scale: 2})`. All 54 JSON files parse, match their root IDs, have no elided children or placeholder flags, and have nonempty PNGs with valid dimensions. Existing CRLF line endings were preserved for 23 JSON files. Canonical desktop, cluster-detail, enrollment-dialog, mobile, dashboard, and representative filter layouts were visually checked with `TakeScreenshot`. The document visitor's known descendant-walk error was avoided by enumerating top-level roots with `skipChildren()` and reading each tree directly. No application tests or lint suites were run.

### Nodes search and filter controls — 2026-11-09

Nodes now follows the Backups toolbar: a plain search input, capped at 320px on desktop and flexible on mobile, plus one right-aligned **Filter** trigger. Cluster and Status move into the existing Clear/Apply popover pattern. The count badge reflects applied Cluster and Status filters only, excludes search, and is hidden at zero. Opening copies applied values into drafts; Clear resets drafts; Apply commits and closes; dismissing discards drafts. Within cluster details, only Status is editable and counted; the fixed cluster remains visible as read-only context.

| Node | Exported change |
| --- | --- |
| `BdmAW` | Desktop Nodes uses the compact search and Filter row. |
| `N2nTf` | Mobile Nodes uses the same controls on one row; sorting and stacked node cards remain below. |
| `J8GzM9` | Cluster-scoped Nodes keeps its fixed-cluster label beside the Status-only Filter trigger. |
| `IBUqJ` | The enrollment design's background inventory toolbar uses the same shared row. |
| `b2JF52` | New reusable Nodes Search and Filters Row, with optional read-only cluster scope. |
| `xrNf3` | New reusable Nodes Filters Button; its component preview demonstrates the two-active-filter badge. |
| `xjVA4` | New reusable Nodes Filters Popover, adapted from Backups with Cluster and Status fields. |

All seven roots have JSON from Pencil `Get` and 2× PNG from `Export`. Toolbar, popover, desktop, fixed-cluster and mobile layouts were inspected using `TakeScreenshot`; the full screens retain their previous dimensions and content. JSON exports retain component references without elided children or editing placeholders. No React source, tests or lint suites were changed or run for this design update.

251 changes: 251 additions & 0 deletions assets/design-export/json/BdmAW.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
{
"type": "frame",
"id": "BdmAW",
"x": 11480,
"y": 63932,
"name": "Screen/Nodes — All clusters",
"context": "Canonical cluster and nodes navigation redesign. Authorized implementation.",
"theme": {
"semantic": "dark"
},
"clip": true,
"width": 1440,
"height": 900,
"fill": "$background/background",
"children": [
{
"id": "grYDZ",
"type": "ref",
"ref": "kKFX9",
"name": "App Sidebar",
"height": "fill_container",
"descendants": {
"iTbd8": {
"fill": "#00000000"
},
"iTbd8/ghUsw": {
"fill": "$foreground/foreground"
},
"iTbd8/bpSQ7": {
"fill": "$foreground/foreground"
},
"VqyiS": {
"fill": "$accent/soft"
},
"VqyiS/Sr2Q5": {
"fill": "$accent/soft-foreground"
},
"VqyiS/Ra9NK": {
"fill": "$accent/soft-foreground"
}
}
},
{
"type": "frame",
"id": "gVOTp",
"name": "Main",
"width": "fill_container",
"height": "fill_container",
"layout": "vertical",
"children": [
{
"id": "r4xdRg",
"type": "ref",
"ref": "gu5WY",
"name": "Top Bar",
"width": "fill_container",
"descendants": {
"Gl7AY": {
"content": "Nodes"
}
}
},
{
"type": "frame",
"id": "q7GH0",
"name": "Body",
"width": "fill_container",
"height": "fill_container",
"layout": "vertical",
"gap": 24,
"padding": 24,
"children": [
{
"id": "ofhIo",
"type": "ref",
"ref": "xCDF7",
"name": "Page Header",
"width": "fill_container",
"descendants": {
"spCmk": {
"content": "Nodes"
},
"VbHwU": {
"content": "Health and capacity across your authorized clusters."
},
"TgdLz": {
"id": "kEFDn",
"type": "ref",
"ref": "cb4rt",
"name": "Add node",
"descendants": {
"U6q13B": {
"content": "Add node"
}
}
}
}
},
{
"type": "frame",
"id": "F9AoB3",
"name": "Node inventory totals",
"width": "fill_container",
"gap": 12,
"children": [
{
"id": "eMCSg",
"type": "ref",
"ref": "ZWcwn",
"name": "NODES READY",
"width": "fill_container",
"descendants": {
"Nd4Wf": {
"content": "NODES READY"
},
"QdcEm": {
"icon": "server"
},
"vj2Ru": {
"content": "4 / 5"
},
"h0n7zo": {
"content": "returned nodes"
}
}
},
{
"id": "Xs472",
"type": "ref",
"ref": "ZWcwn",
"name": "CLUSTERS REPORTING",
"width": "fill_container",
"descendants": {
"Nd4Wf": {
"content": "CLUSTERS REPORTING"
},
"QdcEm": {
"icon": "network"
},
"vj2Ru": {
"content": "2 / 3"
},
"h0n7zo": {
"content": "one cluster unavailable"
}
}
},
{
"id": "OOeYI",
"type": "ref",
"ref": "ZWcwn",
"name": "CPU USAGE",
"width": "fill_container",
"descendants": {
"Nd4Wf": {
"content": "CPU USAGE"
},
"QdcEm": {
"icon": "cpu"
},
"vj2Ru": {
"content": "32%"
},
"h0n7zo": {
"content": "4 nodes reporting"
}
}
},
{
"id": "Jk6tu",
"type": "ref",
"ref": "ZWcwn",
"name": "MEMORY USAGE",
"width": "fill_container",
"descendants": {
"Nd4Wf": {
"content": "MEMORY USAGE"
},
"QdcEm": {
"icon": "memory-stick"
},
"vj2Ru": {
"content": "43%"
},
"h0n7zo": {
"content": "4 nodes reporting"
}
}
}
]
},
{
"id": "f42Iv",
"type": "ref",
"ref": "b2JF52",
"name": "Search and Filters",
"width": "fill_container"
},
{
"type": "frame",
"id": "otKLx",
"name": "Partial inventory notice",
"width": "fill_container",
"fill": "$warning/soft",
"cornerRadius": 12,
"padding": [
12,
16
],
"children": [
{
"type": "text",
"id": "y4htvj",
"name": "Coverage warning",
"fill": "$warning/soft-foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
"content": "Partial inventory · West is unavailable. Showing 5 returned nodes from 2 clusters.",
"lineHeight": 1.45,
"fontFamily": "$typography/font-sans",
"fontSize": 14,
"fontWeight": "normal"
}
]
},
{
"id": "oIZu6",
"type": "ref",
"ref": "I38x1",
"name": "All authorized nodes",
"width": "fill_container"
},
{
"type": "text",
"id": "dpgR1",
"name": "Inventory count",
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
"content": "5 nodes shown · Select a cluster name to open its details.",
"lineHeight": 1.45,
"fontFamily": "$typography/font-sans",
"fontSize": 12,
"fontWeight": "normal"
}
]
}
]
}
]
}
5 changes: 2 additions & 3 deletions assets/design-export/json/Be7ZW.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"id": "Be7ZW",
"x": 4920,
"y": 63932,
"name": "Screen/Standalone — Cluster inventory (No selection)",
"context": "PR #630. Selection is explicit and only registered remotes are listed. No implicit local target; no workload requests until installation and selected target are known.",
"name": "Legacy/Standalone — Cluster inventory selection — Superseded",
"context": "Superseded by Cluster Details Overview, Nodes and Configuration. Backup destinations and registration removal are scoped to cluster Configuration.",
"theme": {
"semantic": "dark"
},
Expand Down Expand Up @@ -175,4 +175,3 @@
}
]
}

2 changes: 1 addition & 1 deletion assets/design-export/json/Cb3eC.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
"content": "Backups"
},
"VbHwU": {
"content": "Snapshots, schedules, and restores across your authorized locations."
"content": "Snapshots, schedules, and restores across your authorized clusters."
},
"TgdLz": {
"type": "frame",
Expand Down
Loading
Loading