Skip to content

feat(cloud): guided per-platform connect + onboarding on the cloud dashboard (0214) - #247

Merged
crs48 merged 6 commits into
mainfrom
claude/cloud-dashboard-guided-connect
Jun 24, 2026
Merged

feat(cloud): guided per-platform connect + onboarding on the cloud dashboard (0214)#247
crs48 merged 6 commits into
mainfrom
claude/cloud-dashboard-guided-connect

Conversation

@crs48

@crs48 crs48 commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Implements exploration 0214 — make the xNet Cloud dashboard teach a just-subscribed user how to connect their apps, instead of just listing management cards.

What changed

apps/cloud/src/dashboard.ts (server-rendered HTML, no bundle):

  • Per-platform Connect tabs (🌐 Web / 🖥️ Desktop / 📱 Mobile), each with tailored steps. Desktop names the exact Settings → Network → Signaling server path and shows the hub URL in a copyable field — the real gap, since the desktop app has no "Connect xNet Cloud hub" button today. Progressive enhancement: the tab bar is hidden until JS arms it; panels render unhidden + headed, so with JS off all three stack readably.
  • Getting-started checklist derived from tenant state (Plan chosen / Hub running / Connect a device). Self-completes once a device binds; a client-set xnet_gs_hidden cookie (read server-side) dismisses it early. No schema change.
  • copyField() + dashScript() — a new always-run vanilla-JS block (liveScript() early-returns without live tiles, which is exactly the unconnected state) for tab switching, copy-to-clipboard (clipboard API → execCommand → select fallback), keyboard tab nav, and the Hide.
  • Help footer (Connect guide / FAQ / Status / Self-host / About Cloud) on every state + a contextual help link by the connect card. marketingUrl threaded into DashboardView and derived into the link set. Hub card Endpoint is now copyable too.

Docs: new /docs/guides/cloud-connect (two-identity model, per-platform steps via Tabs+Steps, the device-code flow as a mermaid sequence, troubleshooting), linked from the docs index, registered in sidebar.mjs, and folded into llms-full.txt. Added id="faq" to the pricing FAQ so #faq links resolve.

Quality

  • 23 unit tests (dashboard.test.ts), all green — tabs/panels, no-JS fallback, ARIA wiring, copyable hub URL + distinct ids, hostile-URL escaping, the state-derived checklist (pending/self-complete/cookie/tenantless/cold), canceled/sleeping states, and help-link derivation (default, misconfigured /, custom origin, trailing slash).
  • Adversarial multi-agent review (22 agents, 7 dimensions, every finding independently verified) ran before this PR. It caught and I fixed: a real progressive-enhancement bug ([hidden] overridden by author display → no-JS fallback defeated), a medium state-logic issue (the "Connected" card showed for canceled/suspended tenants), checklist resurrection for slept/canceled tenants, an incomplete ARIA tab pattern + missing keyboard nav, and a dead #faq anchor. Two findings were correctly dismissed as non-issues.
  • Verified in a browser: checklist + copyable endpoint render, tab switching, keyboard arrow nav + roving tabindex, ARIA wiring, and the [hidden] fix (a hidden .tabs now computes display:none).
  • Site build passes locally (pnpm --filter site build) — which caught a required sidebar.mjs registration that PR CI does not (CI doesn't build the site).
  • eslint clean, prettier clean, type-clean (only env module-resolution cascades in the worktree).

Deferred (tracked as fast-follow)

Per 0214: xnet://connect desktop deep link (D2) and full desktop cloud-claim parity (D3) are a separate Electron PR. The no-JS server-side checklist dismissal is left out (the dashboard is JS-dependent by design).

Redeploy to staging

Merging touches apps/cloud/**, which triggers deploy-cloud.yml to cloud-staging.xnet.fyi when CLOUD_DEPLOY_ENABLED is set (otherwise it's a one-command manual dispatch). Validation steps are in 0214's checklist.

🤖 Generated with Claude Code

xNet Test and others added 5 commits June 24, 2026 08:06
…on the dashboard

Implements the core of exploration 0214 — make the cloud dashboard teach a
just-subscribed user how to connect their apps, not just list management cards.

- connectCard() → per-platform tabs (Web / Desktop / Mobile), each with tailored
  steps. Desktop names the exact "Settings → Network → Signaling server" path and
  shows the hub URL in a copyable field (the real gap: the desktop app has no
  "Connect xNet Cloud hub" button today). Progressive enhancement: tab bar is
  hidden until JS, panels render unhidden + headed so no-JS shows all three stacked.
- getting-started checklist derived from tenant state (plan / hub running /
  connect a device); self-completes and vanishes once a device binds. A client-set
  xnet_gs_hidden cookie (read server-side) lets users dismiss it early — no schema
  change.
- copyField() + dashScript(): a new always-run vanilla-JS block (liveScript()
  early-returns without live tiles, which is exactly the unconnected state) handling
  tab switching, copy-to-clipboard (clipboard API → execCommand → select fallback),
  and the checklist Hide.
- help footer (Connect guide / FAQ / Status / Self-host / About Cloud) on every
  state + a contextual help link by the connect card; marketingUrl threaded into
  DashboardView and derived into the link set.
- hub card Endpoint is now copyable too.

Checks off the implementation items in 0214. Tests + the cloud-connect doc follow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 new dashboard render tests (18 total): per-platform tabs render with all
panels, the no-JS fallback (tab bar hidden, panels visible + headed), the exact
desktop Settings copy + copyable hub URL, distinct copy-field ids (no duplicate),
hostile-URL escaping, the state-derived checklist (shows pending / self-completes /
cookie-dismissed / absent when tenantless), and help-footer link derivation
(default, misconfigured `/`, and custom marketing origin).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A user-facing guide the dashboard links to: the two-identity (billing vs passkey)
model, per-platform connect steps (Web / Desktop / Mobile via Tabs + Steps), the
device-authorization code flow explained with a mermaid sequence diagram, and
troubleshooting. Linked from the docs index guide grid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s, changelog

- Add docs/guides/cloud-connect to site/src/sidebar.mjs (the site build fails the
  llms-full.txt step for any page missing from the sidebar — caught by a local
  `pnpm --filter site build`, since PR CI does not build the site).
- Regenerate site/public/llms-full.txt to include the new guide.
- Add the changelog fragment for the guided-connect dashboard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 22-agent adversarial review (7 dimensions, each finding verified) surfaced these,
all now fixed:

- [hidden] override (real PE bug): `.tabs { display: flex }` and
  `button { display: inline-block }` beat the UA `[hidden]` rule, so the no-JS tab
  bar and the Hide button rendered visible-but-inert — defeating the very fallback
  the tests claim. Added `[hidden] { display: none !important }`. Verified in-browser:
  a hidden `.tabs` now computes `display: none`.
- Canceled/sleeping state (medium): connectCard() gated "Connected — pick up your
  data" on `did` alone, so a canceled (hub torn down) or cold tenant saw it,
  contradicting the adjacent hub/billing cards. Now returns '' when canceled and
  shows an "asleep — opening the app wakes it" message when cold.
- Checklist resurrection: gettingStarted() re-appeared with a false "Hub running"
  step for an already-onboarded tenant whose hub later slept/canceled. Now
  early-returns once a device is bound (monotonic) or the sub is canceled.
- Accessibility: completed the WAI-ARIA tab pattern (id/aria-controls on tabs,
  role=tabpanel/aria-labelledby/tabindex on panels) and added keyboard nav
  (Arrow/Home/End + roving tabindex). Verified in-browser.
- Dead `#faq` anchor: added `id="faq"` to the pricing FAQ section so the dashboard +
  guide links resolve.

Tests: strengthened checklist done-derivation assertions; added cold-tier,
canceled-tenant, sleeping-connected, footer-on-connected, trailing-slash, and ARIA
coverage. 23 tests, all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@crs48
crs48 temporarily deployed to pr-247 June 24, 2026 15:39 — with GitHub Actions Inactive
Tests/lint/typecheck, the rendered-state screenshots, the no-JS + [hidden] fix
verification, copy wiring, and help-link resolution are done. Staging redeploy +
manual dial-in remain (they happen on merge / are operator steps).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@crs48
crs48 temporarily deployed to pr-247 June 24, 2026 15:40 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #247.

github-actions Bot added a commit that referenced this pull request Jun 24, 2026
@crs48
crs48 merged commit e306db6 into main Jun 24, 2026
12 checks passed
@crs48
crs48 deleted the claude/cloud-dashboard-guided-connect branch June 24, 2026 15:56
github-actions Bot added a commit that referenced this pull request Jun 24, 2026
crs48 added a commit that referenced this pull request Jun 24, 2026
…249)

Fast-follow to the guided-connect cloud dashboard
([#247](#247)) — item **D2** from
[exploration
0214](docs/explorations/0214_%5B_%5D_INTUITIVE_CLOUD_DASHBOARD_GUIDED_CONNECT_AND_ONBOARDING.md).
Turns the dashboard's Desktop "paste this URL into Settings" flow into
one click: **"Open in desktop app" → `xnet://connect?hub=<wss>` → the
Electron app pre-fills + connects after you confirm.**

## Security (the crux)
An `xnet://connect` link is an open redirect into a native app, so it's
gated **twice**:
1. **Hard validation in the main process** — new pure, unit-tested
[`apps/electron/src/main/deep-link.ts`](apps/electron/src/main/deep-link.ts):
`parseConnectDeepLink` + `isAllowedHubUrl` require **`wss://`**, an
**xNet-host allowlist** (`xnet.fyi`/`xnet.app` + subdomains;
env-overridable via `XNET_ALLOWED_HUB_HOSTS`, mirroring the existing
share-endpoint policy), **reject embedded credentials**, and bound the
length. Rejects lookalikes (`evilxnet.fyi`, `xnet.fyi.evil.com`),
`ws://`, `http(s)://`.
2. **Explicit user confirmation in the renderer** — new
[`ConnectHubDialog`](apps/electron/src/renderer/components/ConnectHubDialog.tsx)
shows the exact hub and **never auto-connects**; Cancel leaves the
current hub untouched.

## What's in it
- **Main** ([index.ts](apps/electron/src/main/index.ts)):
`handleDeepLink` routes a validated payload to the renderer over
`xnet:cloud-connect`, with cold-launch `pending` delivery on
`did-finish-load`. Preload exposes `onCloudConnect`.
- **Renderer**: on confirm, persist the hub via the new
[`hub-url.ts`](apps/electron/src/renderer/lib/hub-url.ts) (mirrors the
web `setPersistedHubUrl`; also read on boot in
[`ipc-sync-manager.ts`](apps/electron/src/renderer/lib/ipc-sync-manager.ts)
and in **Settings → Network**), then apply it live via
`configureShareSession` (re-points sync, no restart).
- **Dashboard** ([dashboard.ts](apps/cloud/src/dashboard.ts)): replaces
the "coming soon" note with the `xnet://connect` button (https→wss
normalized, URL-encoded), keeping the copy-paste fallback. The button is
emitted **only for an allowlisted xNet hub**, so staging (`*.run.app`)
cleanly shows copy-paste instead of a dead button.

Full in-app device-claim parity (the dual-proof binding) stays as the
**D3** follow-up — passkey + code approval still finish in the
dashboard.

## Tests
- `deep-link.test.ts` (11), `hub-url.test.ts` (4), `dashboard.test.ts`
(+3 → 26). Full cloud unit suite (205) green; eslint clean; prettier
clean.
- Changelog fragment added.

Remaining manual check (can't be done headless): launch a packaged
desktop build and click the button end-to-end.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

1 participant