feat(fleet): single-miner view with embedded ProtoOS proxy#581
feat(fleet): single-miner view with embedded ProtoOS proxy#581mcharles-square wants to merge 2 commits into
Conversation
Hosts the ProtoOS per-miner experience inside the fleet shell and proxies its API calls through to the device, so operators can drill into a single miner from fleet management without leaving the app. - minerproxy handler: session-authenticated reverse proxy to a miner's direct ProtoOS API, with per-device token caching, org-scoped permission enforcement, and http/https client selection - SingleMinerWrapper hosts the ProtoOS single-miner view, passing miner metadata (name/ip/mac/firmware) through route state - MinerHostingContext gains `mode` (direct|fleet) and `metadata` so ProtoOS hooks work standalone or fleet-hosted; useAuthRetry handles both paths - MinerList row click opens the embedded view when available, else the miner URL - proto/sqlc: GetDirectProtoMinerProxyTarget query and the embeddedWebViewAvailable snapshot field, with regenerated code - tests across the new wrapper, proxy handler, hosting context, auth hooks, and KPI/PageHeader/Cooling widgets
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 82c5cc6cd6
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // loading) as safe, producing a burst of 403s on a factory-password device | ||
| // during the window between token validation and status resolution. | ||
| const canAccessProtectedApi = hasAccess === true && isDefaultPasswordActive === false; | ||
| const canAccessProtectedApi = isFleetHosted || (hasAccess === true && isDefaultPasswordActive === false); |
There was a problem hiding this comment.
Bypass remaining miner-token gates in fleet mode
When fleet-hosted, this mounts the authenticated ProtoOS shell and suppresses the direct miner login modal, but some child controls still wait on direct miner JWT state. For example, PowerTarget still calls useAccessToken(!!pausedAuthAction...) and only invokes updateMiningTarget after hasAccess becomes true (client/src/protoOS/components/PageHeader/PowerTarget/PowerTarget.tsx:29,62-69); in the embedded view there are no client-side miner tokens and the modal is hidden, so changing the power target stalls and never reaches the proxy. The same direct-token pattern remains in the General miner tag flow and firmware update path, so these controls need the same fleet-mode bypass added to Power/GlobalActions.
Useful? React with 👍 / 👎.
| const handleViewMiner = useCallback(() => { | ||
| if (minerUrl) { | ||
| window.open(minerUrl, "_blank", "noopener,noreferrer"); | ||
| } | ||
| }, [minerUrl]); | ||
| navigate(`/miners/${encodeURIComponent(deviceIdentifier)}`); |
There was a problem hiding this comment.
Fall back to the external miner URL from the action menu
For miners where embeddedWebViewAvailable is false (for example non-Proto drivers, auth-needed devices, or fleet-node-owned devices), the row click still falls back to miner.url, but this action ignores both minerUrl and the embedded-availability flag and always navigates to /miners/:id. The new proxy only resolves direct paired Proto targets, so using the kebab menu's “View miner” on those rows opens a broken embedded page instead of the previous external web UI (or hiding the action when no URL exists).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Adds a fleet “single-miner” drill-in that embeds the existing ProtoOS UI inside ProtoFleet, backed by a new server-side authenticated reverse proxy to the miner’s ProtoOS API. This spans server authz/target resolution, protobuf + snapshot plumbing for availability, and client hosting-context changes to support both direct and fleet-hosted modes.
Changes:
- Server: introduce
minerproxyreverse-proxy handler with session auth + method/path→permission mapping, and wire it intofleetd. - Data model/API: add
embedded_web_view_availableto miner snapshots (computed in SQL fragments) and expose via FleetManagement proto/service. - Client: add ProtoFleet
SingleMinerWrapper+ route state metadata, and extend ProtoOS hosting/auth flows to behave correctly when fleet-hosted (no direct token injection, no login modal).
Reviewed changes
Copilot reviewed 44 out of 50 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| server/sqlc/queries/miner_service.sql | Adds GetDirectProtoMinerProxyTarget query for proxy target + creds lookup. |
| server/sqlc/queries/device.sql | Extends snapshot select stub to include embedded_web_view_available column. |
| server/internal/handlers/minerproxy/handler.go | New authenticated reverse proxy handler (authn/authz, token cache, forwarding, header hygiene). |
| server/internal/handlers/minerproxy/handler_test.go | Unit tests for permission mapping and response header stripping. |
| server/internal/domain/stores/sqlstores/device.go | Scans the new EmbeddedWebViewAvailable column into snapshot rows. |
| server/internal/domain/stores/sqlstores/device_query_fragments.go | Computes embedded_web_view_available in shared miner snapshot SQL fragments. |
| server/internal/domain/stores/sqlstores/device_integration_test.go | Integration test coverage for embedded_web_view_available computation. |
| server/internal/domain/fleetmanagement/service.go | Plumbs EmbeddedWebViewAvailable into pb.MinerStateSnapshot. |
| server/generated/sqlc/miner_service.sql.go | generated — sqlc output for new query. |
| server/generated/sqlc/device.sql.go | generated — sqlc output for new snapshot column. |
| server/generated/sqlc/db.go | generated — sqlc statement prep/close wiring for new query. |
| server/cmd/fleetd/main.go | Wires the miner proxy handler route into the HTTP mux. |
| proto/fleetmanagement/v1/fleetmanagement.proto | Adds embedded_web_view_available field to MinerStateSnapshot. |
| plugin/proto/go.sum | Dependency checksum updates. |
| plugin/proto/go.mod | Removes direct jwt dependency from plugin/proto module. |
| client/src/protoOS/store/hooks/useAuthRetry.ts | Makes auth retry logic hosting-mode aware (direct vs fleet-hosted). |
| client/src/protoOS/store/hooks/useAuthRetry.test.ts | Tests fleet-hosted behavior for auth retry (no bearer injection/refresh retry). |
| client/src/protoOS/store/hooks/useAuth.ts | Makes auth error handling hosting-mode aware (skip refresh/login modal when fleet-hosted). |
| client/src/protoOS/store/hooks/useAuth.test.ts | Tests fleet-hosted behavior for auth error handling. |
| client/src/protoOS/hooks/useWakeMiner/useWakeMiner.ts | Adjusts wake flow for fleet-hosted mode (no access-token gating). |
| client/src/protoOS/features/settings/components/Cooling/Cooling.tsx | Refactors cooling-mode initialization from store fanMode, improves initial loading behavior. |
| client/src/protoOS/features/settings/components/Cooling/Cooling.test.tsx | New tests for cooling-mode initialization from store value. |
| client/src/protoOS/features/kpis/components/Temperature/Temperature.tsx | Corrects lowest-performer calculations around empty arrays and numeric checks. |
| client/src/protoOS/features/kpis/components/Hashrate/Hashrate.tsx | Corrects lowest-performer calculations around empty arrays and numeric checks. |
| client/src/protoOS/features/kpis/components/Efficiency/Efficiency.tsx | Corrects lowest-performer calculations around empty arrays and numeric checks. |
| client/src/protoOS/contexts/MinerHostingContext/useMinerHosting.ts | Exposes hosting mode + metadata from context hook. |
| client/src/protoOS/contexts/MinerHostingContext/MinerHostingContext.tsx | Adds hosting mode + metadata support; disables securityWorker for fleet-hosted mode. |
| client/src/protoOS/components/PageHeader/Power/PowerWidget.tsx | Fleet-hosted tweaks + improved aria props and sizing classes. |
| client/src/protoOS/components/PageHeader/Power/PowerWidget.test.tsx | Updates mocks and adds tests for fleet-hosted and aria/size behavior. |
| client/src/protoOS/components/PageHeader/PageHeader.test.tsx | Adds PageHeader widget rendering test. |
| client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidgetWrapper.tsx | Fleet-hosted flow for Blink LEDs (no access-token gating). |
| client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidgetWrapper.test.tsx | Tests fleet-hosted behavior for Blink LEDs flow. |
| client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidget.tsx | Improves aria props and sizing classes. |
| client/src/protoOS/components/PageHeader/GlobalActions/GlobalActionsWidget.test.tsx | Tests aria props and sizing behavior. |
| client/src/protoOS/components/AppLayout/AppLayout.tsx | Uses host-provided metadata for nav info panel when fleet-hosted. |
| client/src/protoOS/components/AppLayout/AppLayout.test.tsx | Tests that fleet-hosted mode uses host metadata. |
| client/src/protoOS/components/App/App.tsx | Disables direct onboarding/login gating flows when fleet-hosted. |
| client/src/protoOS/components/App/App.test.tsx | Tests that fleet-hosted mode suppresses ProtoOS login modal behavior. |
| client/src/protoOS/api/hooks/useSystemTag.ts | Normalizes firmware tag response shapes to a consistent string. |
| client/src/protoOS/api/hooks/useSystemTag.test.ts | Tests tag normalization behavior. |
| client/src/protoFleet/features/fleetManagement/components/MinerList/stories/statusMocks.ts | Updates story mocks to include embeddedWebViewAvailable. |
| client/src/protoFleet/features/fleetManagement/components/MinerList/stories/mocks.ts | Updates story mocks to include embeddedWebViewAvailable. |
| client/src/protoFleet/features/fleetManagement/components/MinerList/MinerList.tsx | Row click navigates to embedded view when available; otherwise opens external URL. |
| client/src/protoFleet/features/fleetManagement/components/MinerList/MinerList.test.tsx | Tests embedded navigation vs external URL behavior + route state metadata. |
| client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/SingleMinerActionsMenu.tsx | Changes “View miner” to navigate to embedded route (instead of window.open). |
| client/src/protoFleet/features/fleetManagement/components/MinerActionsMenu/SingleMinerActionsMenu.test.tsx | Updates tests for navigation-based “View miner” behavior. |
| client/src/protoFleet/components/SingleMinerWrapper/SingleMinerWrapper.tsx | Hosts ProtoOS inside fleet shell and sets fleet-proxy baseUrl + metadata. |
| client/src/protoFleet/components/SingleMinerWrapper/routeState.ts | New helper for building and extracting embedded-view route state metadata. |
| client/src/protoFleet/api/generated/fleetmanagement/v1/fleetmanagement_pb.ts | generated — TS protobuf output including embeddedWebViewAvailable. |
| mux.Handle("GET /api/v1/firmware/files", firmwareHandler.NewListFilesHandler(filesService, sessionSvc, userStore)) | ||
| mux.Handle("DELETE /api/v1/firmware/files/{fileId}", firmwareHandler.NewDeleteFileHandler(filesService, sessionSvc, userStore)) | ||
| mux.Handle("DELETE /api/v1/firmware/files", firmwareHandler.NewDeleteAllFilesHandler(filesService, sessionSvc, userStore)) | ||
| mux.Handle("/miners/{deviceIdentifier}/api/v1/{rest...}", minerProxyHandler.NewHandler(conn, sessionSvc, userStore, permissionResolver, encryptSvc)) |
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode == http.StatusUnauthorized && target.passwordEnc.Valid { |
| tokenMu sync.Mutex | ||
| tokens map[string]string | ||
| } |
| <MinerHostingProvider | ||
| baseUrl={safeId} | ||
| baseUrl={`/api-proxy/miners/${safeId}`} | ||
| minerRoot={`/miners/${safeId}`} | ||
| closeButton={(<CloseButton id={displayId} />) as ReactNode} | ||
| mode="fleet" | ||
| metadata={metadata} | ||
| > |
- Share useOpenMinerView so the miner-list row click and the actions-menu "View miner" both gate on embeddedWebViewAvailable: embed when the miner is proxyable, otherwise open its web UI in a new tab. - Show the miner-list name (miner.name) in the single-miner view instead of the device id, sourced from a device-keyed cache so it survives the protoOS loader redirects that drop navigation state. - Hoist SingleMinerWrapper onto the parent route so it stays mounted across tabs; mirror the full-screen modal with a slide-up open/close animation and a secondary icon close button.
Summary
Adds a single-miner view to fleet management: operators can drill into an
individual miner from the miner list and use the full ProtoOS per-miner UI
(KPIs, pools, cooling, logs, settings) without leaving the fleet app. The
ProtoOS UI runs unchanged inside the fleet shell, and its API calls are routed
through a new server-side reverse proxy to the miner's own ProtoOS API.
How it works
The same ProtoOS frontend now runs in two modes via
MinerHostingContext:direct— standalone ProtoOS talking straight to a miner, injecting theaccess token client-side (
securityWorker). Unchanged behavior.fleet— hosted inside fleet management. The client sends requests to/api-proxy/miners/{deviceIdentifier}/…with no client-side token; thefleet server authenticates the session and injects the miner credentials.
sequenceDiagram participant UI as ProtoOS UI (fleet-hosted) participant Proxy as minerproxy.Handler participant DB as Postgres participant Miner as Miner ProtoOS API UI->>Proxy: GET /api-proxy/miners/{id}/api/v1/... Proxy->>Proxy: authenticate session cookie + load effective permissions Proxy->>DB: GetDirectProtoMinerProxyTarget(id, orgID) Proxy->>Proxy: RequirePermission(method+path, siteID) Proxy->>Miner: forward request (+ cached bearer token) Miner-->>Proxy: 401? -> decrypt creds, login, retry once Miner-->>Proxy: response Proxy-->>UI: response (hop-by-hop headers stripped)The proxy is org-scoped and permission-gated: every request is mapped to a
fine-grained
authzpermission (PermMinerRead,PermMinerSetPowerTarget,PermMinerReboot, …) by HTTP method + path, and unmapped mutating routesdefault to an elevated permission. Stored miner passwords are decrypted only
server-side; per-device tokens are cached and refreshed on 401.
What changed
Server
server/internal/handlers/minerproxy/— new authenticated reverse-proxyhandler (session auth, target resolution, permission mapping, token cache,
http/https client selection, header hygiene) + tests
proto/fleetmanagement—embeddedWebViewAvailableon the miner snapshotserver/sqlc/queries—GetDirectProtoMinerProxyTarget+ miner_servicequeries; regenerated sqlc/proto code
server/cmd/fleetd/main.goClient
protoFleet/components/SingleMinerWrapper— hosts the ProtoOS single-minerview, passing miner metadata (name/ip/mac/firmware) through route state;
prefetches the per-miner chunks
MinerList— row click opens the embedded view when available, else theminer URL
protoOS/contexts/MinerHostingContext—mode+metadata;useAuthRetryhandles fleet vs direct token flows
wrapper, hosting context, auth hooks, and widgets
Testing
go build ./...,go vet,golangci-lint, andgo test(incl. the newproxy handler) — all green
tsc --noEmit, eslint, andvitest— all greenclient-typecheck) passed
Reviewer notes
(
minerproxy/handler.go): session validation, the method/path ->permission map, and credential handling.
server/generated/grpc/**files that wereonly showing generator-version drift — the pre-commit hooks normalized them
back to
main, so this PR carries only the realfleetmanagementcodegen.simplifypass run over it first (stableEMPTY_METADATAidentity in the hosting context, a
clientForhelper in the proxy, and atest type cleanup).
Follow-ups (tracked separately, not in this PR)
lowestPerformercalculation shared by theEfficiency/Hashrate/Temperature KPI components into one hook.
device churn over the server's lifetime.