Skip to content

Multi-site: add path-based Fleet site scope routing#516

Open
flesher wants to merge 1 commit into
mainfrom
issue-511
Open

Multi-site: add path-based Fleet site scope routing#516
flesher wants to merge 1 commit into
mainfrom
issue-511

Conversation

@flesher

@flesher flesher commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Reviewable diff: +588/-694 across 42 files (excludes generated, test, and story files).

Summary

This PR makes ProtoFleet site scope URL-addressable by mounting scopable app routes under both explicit all-sites paths like /dashboard and site-prefixed paths like /:siteScope/dashboard. The root path / becomes an app-entry alias that consults the persisted SitePicker preference once and redirects to the preferred dashboard scope. Fleet list pages keep ?site= as a list filter, so filtered all-sites links like /fleet/miners?site=7 continue to work.

How it works

The router defines scopable routes once and mounts them twice: unscoped at /dashboard, /fleet, /groups, /energy, and /activity, and scoped under /:siteScope/.... A route scope provider resolves numeric site-id segments and the reserved unassigned segment, while rejecting unknown prefixes back to /.

useActiveSite() reads route scope when present, validates loaded site IDs, and mirrors valid route scope into the Zustand UI store. The SitePicker now navigates to the equivalent scoped path when the current page is scopable, or to the scoped Dashboard landing from non-scopable pages. Fleet tabs, saved-view navigation, group links, and building links use shared scope helpers so they preserve the active path prefix, while explicit ?site= deep links remain all-sites filtered links.

Fleet list requests compose path scope with the list-level ?site= filter as an intersection. /fleet/miners?site=7 means all-sites scope filtered to site 7; /8/fleet/miners?site=7 intentionally resolves to an empty server filter.

Diagrams

flowchart TD
  A["Browser URL"] --> B{"Scopable route?"}
  B -->|"/dashboard, /fleet, /groups, /energy, /activity"| C["Route scope: all sites"]
  B -->|"<site-id>/..."| D["Route scope: site id"]
  B -->|"/unassigned/..."| E["Route scope: unassigned"]
  C --> F["useActiveSite"]
  D --> F
  E --> F
  F --> G["SitePicker label and persisted preference"]
  F --> H["Fleet list request scope"]
  H --> I["Intersect with ?site= list filter"]
Loading
sequenceDiagram
  participant User
  participant Picker as SitePicker
  participant Router
  participant Store as Zustand UI store
  participant List as Fleet list page

  User->>Picker: Select Site 7
  Picker->>Router: Navigate to scoped current page or /7/dashboard
  Router->>Store: Mirror route scope
  Router->>List: Render with route scope Site 7
  List->>List: Apply Site 7 scope intersected with query filters
Loading

Areas of the code involved

Area / package / file What changed Why it matters for review
client/src/protoFleet/router.tsx Adds / entry redirect and dual-mounts Dashboard, Fleet, Groups, Energy, and Activity under unscoped and /:siteScope parents Confirms route ranking and separates app entry from explicit all-sites URLs
client/src/protoFleet/routing/siteScope.tsx Adds scope parsing, prefixing, unprefixing, and route providers Central seam for future slug transport changes
PageHeader/SitePicker Makes useActiveSite() route-aware and makes picker selections navigate Establishes URL-as-source-of-truth behavior
NavigationMenu, navItems.ts, FleetLayout, FleetViewTabs Preserves current scope for primary nav, Fleet tab selection, and saved views Prevents scoped users from being dropped back to all-sites paths
Fleet.tsx, RacksPage.tsx, FleetBuildingsPage.tsx Composes path scope with ?site= filters using intersection semantics Separates global scope from saved-view/list filters
redirectLoaders.ts, useDeviceSetListState.ts Preserves stored scope through legacy Fleet aliases unless an explicit ?site= filter is present, and short-circuits explicit empty site intersections Keeps legacy links scoped, keeps filtered deep links all-sites scoped, and avoids invalid sentinel IDs
Tests Adds helper, hook, picker, and filter coverage Locks down route precedence and filter composition

Key technical decisions & trade-offs

Decision Trade-off
Use numeric site IDs as the path segment for this plumbing PR Avoids blocking on the separate slug schema/migration work
Add route plumbing for Dashboard, Groups, Energy, and Activity before their data is scoped Lets URL preference and nav behavior settle before follow-up data-scoping PRs
Keep ?site= as a list filter Preserves existing filtered deep links from the Sites table and saved-view semantics
Use a client-only matchNone flag for disjoint scope/filter intersections Avoids sending invalid sentinel IDs while still rendering empty results

Testing & validation

  • npm run lint
  • npx tsc --noEmit
  • npm test -- SitePicker useActiveSite FleetLayout siteScope siteFilter NavigationMenu SecondaryNavigation SiteList BuildingList --run

Not covered here: slug persistence, scoping Dashboard/Groups/Energy/Activity data fetching, and removing legacy ?site= filter behavior.

Fixes #511

@flesher flesher requested a review from a team as a code owner June 19, 2026 18:34
Copilot AI review requested due to automatic review settings June 19, 2026 18:34
@github-actions github-actions Bot added javascript Pull requests that update javascript code client labels Jun 19, 2026
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Note: This is an automated security-focused code review generated by Codex.
It should be used as a supplementary check alongside human review.
False positives are possible - use your judgment.

Scope summary

  • Reviewed pull request diff only (1e923eb43ca2722b661a0ea0ecadecb41bed08c3...40b6ce593a7051a9c0dba4defbe37463b3f019fc, exact PR three-dot diff)
  • Model: gpt-5.5

💡 Click "edited" above to see previous reviews for this PR.


Review Summary

Overall Risk: HIGH

Findings

[HIGH] Site-scoped URLs are enabled for pages that still use org-wide data/actions

  • Category: Frontend | Reliability | Auth
  • Location: client/src/protoFleet/router.tsx:162
  • Description: The new scoped route tree exposes /7/dashboard, /7/groups, /7/energy, and /7/activity, and navigation marks those entries as scopable. However those pages still issue unscoped/org-wide requests. For example, Dashboard calls useFleetCounts, useComponentErrors, and useTelemetryMetrics without a site filter; Activity builds only event/user/scope filters; Energy renders curtailment management with no route-site constraint. This makes the URL and SitePicker indicate “site 7” while the page can still display or act on whole-fleet data.
  • Impact: Operators can make decisions from misleading scoped pages. The Energy case is especially risky: a user navigating via a site-scoped Energy link could believe they are acting within one site while starting/stopping curtailment across the fleet. This can also expose cross-site operational data in dashboards/activity/groups if server-side permissions allow org-wide reads.
  • Recommendation: Only include routes in the scoped route set once their data and mutations accept and enforce the route site scope. Until then, remove dashboard, groups, energy, and activity from the scoped route factory/helper/nav scoping, or add explicit site filters and action scoping for each page before generating scoped URLs.

Notes

No SQL injection, command injection, protobuf wire-format, plugin, infrastructure, Rust, or cryptostealing/pool-hijack issues were visible in this frontend-focused diff.


Generated by Codex Security Review |
Triggered by: @flesher |
Review workflow run

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces path-based “site scope” routing for ProtoFleet by mounting Fleet under both /fleet/... (all-sites scope) and /:siteScope/fleet/... (single-site or unassigned scope), and makes the URL the source of truth for the active scope while keeping ?site= as a list-level filter that composes with the path scope.

Changes:

  • Added site-scope routing helpers + a route-scope provider/layout to parse/prefix/unprefix Fleet paths.
  • Updated Fleet navigation (primary nav + tabs/saved views) to preserve the current site scope in generated links.
  • Updated Fleet list pages to apply scope ∩ filter semantics between the path scope and ?site=.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
client/src/protoFleet/routing/siteScope.tsx Adds scope parsing and scoped/unscoped path helpers plus scope-providing layouts.
client/src/protoFleet/routing/siteScope.test.ts Adds unit tests for scope parsing and path prefix/strip behavior.
client/src/protoFleet/router.tsx Dual-mounts Fleet routes under unscoped /fleet and scoped /:siteScope/fleet.
client/src/protoFleet/features/fleetManagement/pages/RacksPage.tsx Applies path-scope ∩ ?site= list-filter semantics for racks requests.
client/src/protoFleet/features/fleetManagement/pages/FleetBuildingsPage.tsx Applies path-scope ∩ ?site= list-filter semantics for buildings requests.
client/src/protoFleet/features/fleetManagement/components/FleetViewTabs/FleetViewTabs.tsx Ensures saved-view navigation preserves the active site scope.
client/src/protoFleet/features/fleetManagement/components/FleetLayout/FleetLayout.tsx Makes Fleet tab routing scope-aware (including redirects / tab selection).
client/src/protoFleet/features/fleetManagement/components/Fleet/Fleet.tsx Applies scope ∩ filter composition for miner list + total count queries.
client/src/protoFleet/config/navItems.ts Marks Fleet primary nav item as “scopable”.
client/src/protoFleet/components/PageHeader/SitePicker/useActiveSite.ts Makes useActiveSite() prefer route scope and mirror it into the persisted store.
client/src/protoFleet/components/PageHeader/SitePicker/useActiveSite.test.ts Tests route-scope source-of-truth behavior and store mirroring.
client/src/protoFleet/components/PageHeader/SitePicker/SitePicker.tsx Makes picker selections navigate to the equivalent scoped Fleet URL.
client/src/protoFleet/components/PageHeader/SitePicker/SitePicker.test.tsx Tests picker-triggered navigation preserves the current Fleet path.
client/src/protoFleet/components/PageHeader/SitePicker/siteFilter.ts Adds intersectSiteFilters() to implement scope ∩ filter semantics.
client/src/protoFleet/components/PageHeader/SitePicker/siteFilter.test.ts Adds tests for filter composition behavior (including match-nothing sentinel).
client/src/protoFleet/components/PageHeader/SitePicker/index.ts Exposes intersectSiteFilters from the SitePicker module barrel.
client/src/protoFleet/components/NavigationMenu/Navigation.tsx Prefixes scopable nav links with the active scope and fixes active-item detection for scoped Fleet paths.

Comment thread client/src/protoFleet/router.tsx
Comment thread client/src/protoFleet/components/PageHeader/SitePicker/siteFilter.test.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3818968bf9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread client/src/protoFleet/router.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5596a22e47

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread client/src/protoFleet/router.tsx
@flesher

flesher commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Security review triage: the latest Codex Security Review on 26858dd has one remaining MEDIUM finding about scoped Dashboard/Groups/Energy/Activity URLs still rendering org-wide data. That is intentionally staged route plumbing per product direction for this PR; the site picker remains feature-flagged off until those pages receive end-to-end data scoping in follow-up work. The actionable filter-sentinel issue from the earlier review was fixed by replacing the invalid siteIds: [0n] request with a client-only matchNone short-circuit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26858dd9e2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d44a7cc99a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread client/src/protoFleet/router.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f938c87b7d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread client/src/protoFleet/components/PageHeader/SitePicker/useActiveSite.ts Outdated
@flesher flesher force-pushed the issue-511 branch 2 times, most recently from 85d0aa4 to a3a6036 Compare June 19, 2026 19:31

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a3a60364c0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread client/src/protoFleet/routing/siteScope.tsx
Comment thread client/src/protoFleet/features/fleetManagement/pages/RacksPage.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d657ae9bf1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 718481bb46

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@flesher flesher force-pushed the issue-511 branch 2 times, most recently from abf0c49 to e6348a5 Compare June 19, 2026 19:55

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e6348a53c0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread client/src/protoFleet/router.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: edaf501a18

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread client/src/protoFleet/components/PageHeader/PageHeader.tsx Outdated
@flesher flesher force-pushed the issue-511 branch 2 times, most recently from b076001 to bcdd13f Compare June 19, 2026 20:47

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bcdd13f02d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread client/src/protoFleet/features/fleetManagement/pages/RacksPage.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 40b6ce593a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const createScopableRoutes = (absolute: boolean) => [
...(absolute ? [] : [{ index: true, element: <Navigate to="dashboard" replace /> }]),
createRoute(absolute ? "/dashboard" : "dashboard", <Dashboard />, { bg: "surface-5" }),
createFleetRoute(absolute ? "/fleet" : "fleet"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve selected site when leaving mismatched site details

Because this mounts bare /fleet as the explicit all-sites Fleet route, the existing mismatch guard in SiteDetailPage (navigate("/fleet")) no longer carries the selected site. If an operator with stored/selected site 8 opens /sites/7, that guard sends them to /fleet, this all-sites route lets useActiveSite mirror {kind:"all"} into the store, and Fleet renders/redirects in all-sites scope instead of the selected site; route that bounce through the selected scope (for example via scopedPath) before treating bare /fleet as all-sites.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-site: path-based site scoping (routing plumbing)

2 participants