Skip to content

feat(ui): Setup GCP — pairing requests + channels admin (Pieza 2/3)#1

Merged
sitiouno merged 1 commit into
mainfrom
feat/setup-gcp-pairing-channels
Apr 26, 2026
Merged

feat(ui): Setup GCP — pairing requests + channels admin (Pieza 2/3)#1
sitiouno merged 1 commit into
mainfrom
feat/setup-gcp-pairing-channels

Conversation

@sitiouno
Copy link
Copy Markdown
Collaborator

Summary

Adds a Setup GCP item to the Console sidebar of the openclaw-office fork. From this page, HQ operators can:

  • Approve / reject pending pairing requests from new branch nodes that have published themselves over the Tailscale VPN.
  • Manage notification channels (Telegram first) backed by GCP Secret Manager — create, set-secret, test, enable/disable, delete.

This is Pieza 2/3 in the SitioUno alignment plan. The backend (Pieza 1) is sitiouno/gcloud-office#1 — that PR adds /v1/pairing/* and /v1/channels/* to the fleet-registry-api Cloud Run service. This PR is the operator-facing UI that drives those endpoints.

Pieza Repo Scope Status
1 sitiouno/gcloud-office API + SQL + scripts merged-pending
2/3 this PR Setup GCP UI (pairing list + channels CRUD) here
4 sitiouno/gcloud-office Capablanca HQ runner notify hookup next

Architecture decisions

  • REST, not gateway WebSocket. The existing gateway adapter has no REST proxy and is scoped to a single OpenClaw node, while pairing/channels are HQ-Cloud admin operations against a separate Cloud Run service. The new client lives in src/lib/registry-api-client.ts and is invoked directly from a Zustand store; it does not go through getAdapter().
  • Config resolution mirrors the gateway pattern. VITE_REGISTRY_API_URL / VITE_REGISTRY_API_TOKEN at build time, or window.__OPENCLAW_CONFIG__.registryApiUrl/Token injected at runtime — same pattern src/App.tsx uses for gatewayUrl/gatewayToken. No URL hard-coded.
  • Token never touches localStorage. The admin token is a server-side credential; it lives only in the env / runtime config object.
  • Secrets stay on the server. The UI sends secret_value to POST /v1/channels/{id}/secret, then forgets it — the field is local component state, not Zustand state, and is cleared after submit. A read-only notice on the page reminds the operator.
  • Existing Channels page is preserved. That page wraps the Gateway-managed transport channels (WhatsApp pairing, etc.), which are a different concept from the registry-managed alerting channels. To avoid confusing the two, the Channels page now shows a small banner that links to Setup GCP for Telegram alerts. The telegram entry in src/lib/channel-schemas.ts was extended with the required chatId text field, so any future Channels-side flow that re-uses that schema picks it up automatically.

Files added

  • src/lib/registry-api-client.ts — typed REST client (pairing.*, channels.*), env/runtime config resolution, RegistryApiError + RegistryApiNotConfiguredError.
  • src/lib/__tests__/registry-api-client.test.ts — covers config resolution, bearer header, JSON body, error mapping, and not-configured behavior.
  • src/store/console-stores/setupgcp-store.ts — Zustand store with separate loading/error/in-flight state for pairing and channels.
  • src/components/pages/SetupGcpPage.tsx — page shell with two tabs (Pairing Requests / Notification Channels) and the Secret Manager notice.
  • src/components/pages/SetupGcpPage.test.tsx — render smoke + not-configured hint test.
  • src/components/console/setupgcp/PairingRequestsView.tsx — status-filter chips, expandable rows, Approve/Reject (with reason) actions.
  • src/components/console/setupgcp/ChannelsAdminView.tsx — channel list, Create dialog (with chat_id + bot token for telegram), set-secret form, test button with inline result, enable/disable, delete.
  • src/components/console/setupgcp/SecretManagerNotice.tsx — read-only "secrets stay in GCP" panel.

Files modified

  • src/components/layout/ConsoleLayout.tsx — new Cloud sidebar item between Cron and Settings.
  • src/App.tsx/setup-gcp route + PAGE_MAP entry.
  • src/gateway/types.tsPageId gains "setupGcp".
  • src/components/pages/ChannelsPage.tsx — banner pointing to Setup GCP for Telegram alerts.
  • src/lib/channel-schemas.ts — telegram now requires chatId alongside botToken.
  • src/i18n/locales/{en,zh}/layout.jsonconsoleNav.setupGcp, pageTitles.setupGcp.
  • src/i18n/locales/{en,zh}/console.json — full setupGcp.* tree, channels.fields.chatId, channels.placeholders.chatId, channels.gcpNotice.*.
  • src/vite-env.d.ts — declares VITE_REGISTRY_API_URL + VITE_REGISTRY_API_TOKEN.
  • .env.example — example block for the new env vars (no real values).

Validation

  • npm run typecheck — clean.
  • npm test483/483 passing (57 test files), including the 7 new tests.
  • npm run build — production build succeeds.
  • oxlint src/ — no new warnings introduced (3 pre-existing warnings unchanged).
  • No new runtime dependencies added; only existing lucide-react, react-i18next, react-router-dom, zustand.

Test plan

  • Build + serve locally with VITE_REGISTRY_API_URL and VITE_REGISTRY_API_TOKEN pointing at the staging fleet-registry-api → Setup GCP loads pending requests, approve flow returns 200.
  • Create a Telegram channel from the UI → secret_ref becomes notification-channel-telegram-{id}, Test button delivers a message.
  • Toggle enable/disable → reflects on next list fetch.
  • Without env vars set → page shows the "Registry API not configured" hint and does not 401-spam.
  • Inject window.__OPENCLAW_CONFIG__ = { registryApiUrl, registryApiToken } at runtime via bin/openclaw-office.js template → behaves identically to the env-var path.

Screenshots

(to be added once the staging fleet-registry-api PR #1 is deployed and a real Telegram channel can be exercised)

Out of scope

  • Wiring the Capablanca HQ runner to actually consume notification_channels (Pieza 4, gcloud-office repo).
  • A REST-over-gateway proxy that would let regular branch operators see channels (not needed; this is HQ-only).
  • Smoke-test from the UI (POST /smoke-test exists in the API but requires the operator to paste the delegation token; deferred to a follow-up).

🤖 Generated with Claude Code

Adds a "Setup GCP" item to the Console sidebar that lets HQ operators
manage the SitioUno fleet against the new fleet-registry-api endpoints
(see sitiouno/gcloud-office#1):

  - View / approve / reject pending pairing requests from new branch
    nodes that join over Tailscale.
  - Create, configure, set-secret, test, enable/disable, and delete
    notification channels (Telegram first), with secret values written
    to GCP Secret Manager via the registry API and never persisted in
    the browser.

The page talks to the registry API over REST (admin bearer), separate
from the existing gateway WebSocket adapter which has no REST proxy.
Base URL + token are resolved from VITE_REGISTRY_API_URL/TOKEN or from
runtime injection via window.__OPENCLAW_CONFIG__.registryApiUrl/Token,
mirroring how the gateway URL/token are resolved in src/App.tsx.

Channels page now also surfaces a small banner pointing to Setup GCP
for Telegram alerts, and the telegram channel-schema gains the required
chatId field so any future channel-config UI built on the existing
schema picks it up automatically.

i18n keys added under setupGcp.* and consoleNav.setupGcp in both en
and zh; channels.fields.chatId / placeholders.chatId added; channels.gcpNotice.* added.

Tests:
  - registry-api-client URL/header/body resolution + error mapping
  - SetupGcpPage smoke render + not-configured hint

build / typecheck / vitest (483 tests) all green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@sitiouno sitiouno merged commit 19cdd9e into main Apr 26, 2026
@sitiouno sitiouno deleted the feat/setup-gcp-pairing-channels branch April 26, 2026 08:50
sitiouno added a commit that referenced this pull request Apr 26, 2026
…bearer) (#2)

Adjust the registry-API client introduced in PR #1 to talk to a NEW local
HQ sidecar on the Tailscale tailnet (`http://openclaw-hq:8781`) instead of
a public Cloud Run endpoint, and remove the bearer-token plumbing.

Why
- Shipping a long-lived admin token in the browser bundle (even via runtime
  injection) is a meaningful security regression: any XSS, devtools peek,
  or accidental log capture leaks an admin credential. This was flagged by
  Jean as exposed surface.
- The architecture rework moves all admin traffic inside the Tailscale VPN.
  The HQ sidecar listens only on the tailnet and validates the source
  identity server-side via `tailscale whois`. The VPN is the trust
  boundary, not a token.

Changes
- `src/lib/registry-api-client.ts`:
  - Default base URL `http://openclaw-hq:8781` (override via
    `VITE_REGISTRY_API_URL` or `window.__OPENCLAW_CONFIG__.registryApiUrl`).
  - Drop all token handling: no `Authorization` header, no
    `VITE_REGISTRY_API_TOKEN`, no `__OPENCLAW_CONFIG__.registryApiToken`,
    no token field on the resolved config.
  - Keep all typed methods (pairing.list/get/approve/reject/smokeTest,
    channels.list/get/create/update/delete/setSecret/test).
  - Add `notify.broadcast({ scope, text })` -> POST `/v1/notify`.
- Tests:
  - `registry-api-client.test.ts`: drop bearer-header assertions; add
    explicit assertions that no `Authorization` header is sent; add
    coverage for the new Tailscale default and for `notify.broadcast`.
  - `SetupGcpPage.test.tsx`: rewrite the "not configured" expectation —
    with the new default the base URL is always populated, so we instead
    assert the default `openclaw-hq:8781` URL is rendered. Stub `fetch`
    so the auto-loaded pairing/channels lists don't hit the network.
- `src/vite-env.d.ts`: drop `VITE_REGISTRY_API_TOKEN`, document the new
  default for `VITE_REGISTRY_API_URL`.
- `.env.example`: drop the token line, point the URL example at the
  Tailscale HQ sidecar, explain the trust model in the comment.
- i18n (`en` + `zh` `setupGcp.notConfigured.body`): rephrase to mention
  only the URL knob and the VPN trust boundary.

The `ChannelsPage` GCP banner is unchanged — it already links to
`/setup-gcp` with copy that does not reference the bearer or the external
URL.

Validation
- `npm run typecheck`: clean.
- `npm test`: 483/483 passing across 57 files.
- `npm run build`: succeeds.

Co-authored-by: Jean Garcia (via Codex Local) <jean@flexipos.us>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.

2 participants