Skip to content

feat(fleet): single-miner view with embedded ProtoOS proxy#581

Draft
mcharles-square wants to merge 2 commits into
mainfrom
feat/fleet-single-miner-view
Draft

feat(fleet): single-miner view with embedded ProtoOS proxy#581
mcharles-square wants to merge 2 commits into
mainfrom
feat/fleet-single-miner-view

Conversation

@mcharles-square

Copy link
Copy Markdown
Collaborator

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 the
    access 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; the
    fleet 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)
Loading

The proxy is org-scoped and permission-gated: every request is mapped to a
fine-grained authz permission (PermMinerRead, PermMinerSetPowerTarget,
PermMinerReboot, …) by HTTP method + path, and unmapped mutating routes
default 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-proxy
    handler (session auth, target resolution, permission mapping, token cache,
    http/https client selection, header hygiene) + tests
  • proto/fleetmanagementembeddedWebViewAvailable on the miner snapshot
  • server/sqlc/queriesGetDirectProtoMinerProxyTarget + miner_service
    queries; regenerated sqlc/proto code
  • route wiring in server/cmd/fleetd/main.go

Client

  • protoFleet/components/SingleMinerWrapper — hosts the ProtoOS single-miner
    view, 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 the
    miner URL
  • protoOS/contexts/MinerHostingContextmode + metadata; useAuthRetry
    handles fleet vs direct token flows
  • KPI / PageHeader / Cooling widgets + broad test coverage across the new
    wrapper, hosting context, auth hooks, and widgets

Testing

  • go build ./..., go vet, golangci-lint, and go test (incl. the new
    proxy handler) — all green
  • Client tsc --noEmit, eslint, and vitest — all green
  • Pre-commit and pre-push hooks (goimports, buf-lint, server-lint,
    client-typecheck) passed

Reviewer notes

  • Primary review surface is the proxy's auth + permission model
    (minerproxy/handler.go): session validation, the method/path ->
    permission map, and credential handling.
  • The diff excludes ~57 unrelated server/generated/grpc/** files that were
    only showing generator-version drift — the pre-commit hooks normalized them
    back to main, so this PR carries only the real fleetmanagement codegen.
  • This branch had a simplify pass run over it first (stable EMPTY_METADATA
    identity in the hosting context, a clientFor helper in the proxy, and a
    test type cleanup).

Follow-ups (tracked separately, not in this PR)

  • Extract the duplicated lowestPerformer calculation shared by the
    Efficiency/Hashrate/Temperature KPI components into one hook.
  • Bound the proxy's per-device token cache (LRU/TTL) so it doesn't grow with
    device churn over the server's lifetime.

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
@mcharles-square mcharles-square requested a review from a team as a code owner June 25, 2026 16:30
Copilot AI review requested due to automatic review settings June 25, 2026 16:30
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code client server shared labels Jun 25, 2026

@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: 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +113 to +114
const handleViewMiner = useCallback(() => {
if (minerUrl) {
window.open(minerUrl, "_blank", "noopener,noreferrer");
}
}, [minerUrl]);
navigate(`/miners/${encodeURIComponent(deviceIdentifier)}`);

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 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 👍 / 👎.

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

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 minerproxy reverse-proxy handler with session auth + method/path→permission mapping, and wire it into fleetd.
  • Data model/API: add embedded_web_view_available to 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.

Comment thread server/cmd/fleetd/main.go
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 {
Comment on lines +59 to +61
tokenMu sync.Mutex
tokens map[string]string
}
Comment on lines 66 to 72
<MinerHostingProvider
baseUrl={safeId}
baseUrl={`/api-proxy/miners/${safeId}`}
minerRoot={`/miners/${safeId}`}
closeButton={(<CloseButton id={displayId} />) as ReactNode}
mode="fleet"
metadata={metadata}
>
@mcharles-square mcharles-square marked this pull request as draft June 25, 2026 19:07
- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code server shared

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants