Skip to content

feat: xNet Cloud onboarding UI — marketing, signup→provision, claim flow (0192) - #140

Merged
crs48 merged 7 commits into
mainfrom
feat/cloud-onboarding-ui-0192
Jun 17, 2026
Merged

feat: xNet Cloud onboarding UI — marketing, signup→provision, claim flow (0192)#140
crs48 merged 7 commits into
mainfrom
feat/cloud-onboarding-ui-0192

Conversation

@crs48

@crs48 crs48 commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Implements docs/explorations/0192 — the "face" layer that 0180 found missing: the marketing/pricing surface, the signup→checkout→provision spine, the dashboard, and the connect-your-hub flow.

Hosting model (answers the "where + cheaply" question)

  • Marketing + pricing → static Astro on the existing GitHub Pages site ($0).
  • Auth callback, checkout, webhook, dashboard, claim → the xnet-cloud Hono service (scale-to-zero Cloud Run), served same-origin so the sealed session cookie needs no CORS.

Slice 1 — Marketing & pricing (static)

  • site/src/data/pricing.ts (MIT — mirrors PLAN_CATALOG/PLAN_PRICING, so the static build never imports the FSL @xnetjs/cloud).
  • /cloud (offering + 4-step onboarding) and /cloud/pricing (Free/Personal/Family/Team/Enterprise grid + FAQ); Nav "Cloud" link; roadmap entry.

Slice 2 — Signup → checkout → provision + dashboard

  • Fills 0180's holes: GET /auth/callback (seal an httpOnly signed session), POST /checkout, POST /webhook (verify → provision/suspend), POST /portal, and a server-rendered /dashboard.
  • provisionForBilling provisions the hub with did='' (bound later via the claim flow); suspendTenant (cancel) retains the R2 replica; deleteTenant (delete data).
  • TenantBillingGateway port + keyless FakeTenantBillingGateway — the plan-subscription surface, kept distinct from cloud AI metering and the hub's end-user billing (the conflation trap from 0192).

Slice 3 — Connect the app + account management

  • Device-grant "claim your hub" (RFC 8628): /device/start + /device/token (app polls) and /claim (dashboard approval) → dual-proof bindDataIdentity. The app never embeds WorkOS.
  • Fixes the inert xnet:hub-url settingApp.tsx now reads what Settings writes (lib/hub-url.ts).
  • lib/cloud-claim.ts app-side client; Settings → Network gains an "xNet Cloud" group (see plans / open dashboard).

Verification

  • 47 tests pass: 40 apps/cloud (signup funnel, device-grant claim, session sealing, signature verify) + 7 apps/web lib.
  • apps/cloud + apps/web typecheck clean; eslint + prettier clean; astro build green.
  • Dashboard, claim page, and marketing/pricing pages screenshotted (light + dark).

Deferred (infra / not implementable in-repo) — see the doc's Implementation Status

Durable stores (still in-memory), real Stripe + @xnetjs/identity adapters, Cloud Run deploy, and WorkOS/Stripe dashboard configuration. The onboarding state-machine integration of the claim flow is also deferred (the in-app entry lives in Settings) to protect the passkey-gated editor-ux e2e.

🤖 Generated with Claude Code

xNet Test and others added 5 commits June 17, 2026 09:05
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Static Astro pages on GitHub Pages — the cheapest, lowest-risk slice of the
xNet Cloud "face". Single-sources tiers/prices/steps from a new MIT
site/src/data/pricing.ts (mirrors PLAN_CATALOG + PLAN_PRICING, so the static
build never imports the FSL @xnetjs/cloud package).

- /cloud: offering, "what you get", 4-step onboarding flow, enterprise CTA
- /cloud/pricing: Free/Personal/Family/Team/Enterprise grid + FAQ
- Nav gains a "Cloud" link; roadmap gains a managed-hosting "Now" item
- "Get started" deep-links to https://cloud.xnet.fyi/auth/start?plan=…

Verified: astro build green; both pages render light + dark.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fills the holes 0180 flagged in the control plane: the auth callback, the
checkout funnel, and an authenticated dashboard — served same-origin from the
Hono service so the sealed session cookie needs no CORS.

- session.ts: HMAC-signed session cookie (sealSession/readSession, dep-free)
- billing-gateway.ts: TenantBillingGateway port + keyless FakeTenantBillingGateway
  (plan-subscription surface, kept distinct from cloud AI metering + hub billing)
- server.ts routes: /auth/callback (seal session), /checkout, /portal, /webhook
  (verify→provision/suspend), /dashboard, /logout, /account/delete-data
- control-plane.ts: provisionForBilling (provision hub, did='' — bound later via
  the claim flow), getTenantForBilling, suspendTenant (cancel), deleteTenant;
  TenantRecord gains subscriptionStatus; TenantStore gains delete()
- dashboard.ts: server-rendered HTML (hub status, connect-app steps, billing,
  danger zone with the custodial vs non-custodial deletion asymmetry)
- index.ts wires the gateway + session/base/marketing config from env

Verified: 34 apps/cloud tests pass (21 new), typecheck + eslint + prettier clean,
dashboard rendered + screenshotted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(0192 slice 3)

Connects the app to a managed hub and fixes the bug where the hub-URL setting
was written but never read.

Control plane (RFC 8628 "claim your hub"):
- device-grant.ts: MemoryDeviceGrantStore + unambiguous user codes (injectable RNG)
- routes /device/start + /device/token (app polls), /claim GET+POST (dashboard
  approval) — the app shows a code, the signed-in user approves it (proves billing),
  the app polls with a signed DID challenge (proves data identity)
- control-plane.bindDataIdentity: dual-proof bind that stamps the DID onto the
  tenant provisioned earlier with did=''
- dashboard.ts: claim-form + result pages (shared chrome)

Web app:
- lib/hub-url.ts: persistedHubUrl/setPersistedHubUrl — App.tsx now READS the
  hub URL the Settings panel writes (the inert-setting bug from 0192)
- lib/cloud-claim.ts: app-side device-grant client (start/poll), fetch-injectable
- Settings → Network: shared hub-url helper + an "xNet Cloud" group (see plans +
  open dashboard). Cloud-tenant billing intentionally lives in the dashboard, not
  useBilling() (which is the hub's separate end-user surface)

Verified: 47 tests pass (40 cloud incl. 6 claim, 7 web lib), both apps typecheck,
eslint + prettier clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Marks the implemented code items done and adds an honest Implementation Status
note distinguishing them from the deferred infra/deploy items (durable stores,
real Stripe + @xnetjs/identity adapters, Cloud Run deploy, WorkOS/Stripe setup).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@crs48
crs48 temporarily deployed to pr-140 June 17, 2026 16:39 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #140.

github-actions Bot added a commit that referenced this pull request Jun 17, 2026
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

Screens

✏️ Settings _(SSIM 0.953)_
before after diff
before after diff

Auto-captured by CI · run. Informational — not a blocking check.

github-actions Bot added a commit that referenced this pull request Jun 17, 2026
@crs48
crs48 temporarily deployed to pr-140 June 17, 2026 16:46 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jun 17, 2026
github-actions Bot added a commit that referenced this pull request Jun 17, 2026
@crs48
crs48 temporarily deployed to pr-140 June 17, 2026 16:52 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jun 17, 2026
@crs48
crs48 merged commit 71491ec into main Jun 17, 2026
9 of 10 checks passed
github-actions Bot added a commit that referenced this pull request Jun 17, 2026
github-actions Bot added a commit that referenced this pull request Jun 17, 2026
Comment thread site/src/data/pricing.ts
*/

/** Origin of the xNet Cloud control plane (auth callback, checkout, dashboard). */
export const CLOUD_ORIGIN = 'https://cloud.xnet.fyi'
}

const esc = (s: string): string =>
s.replace(/[&<>"']/g, (c) =>
crs48 added a commit that referenced this pull request Jun 17, 2026
Implements exploration
[0196](docs/explorations/0196_[x]_IMAGE_RICH_INTERACTIVE_CHANGELOG_PAGE.md)
— makes the changelog page show the actual UI that shipped, pulled
straight from CI's visual captures, plus the interactive touches that
make it browsable. **Zero new dependencies.**

## The headline: auto-pulled diff galleries
Every PR's visual-capture run publishes a durable manifest at
`xnet.fyi/visuals/pr/<N>/diff-manifest.json`. Since each changelog entry
already stores a `pr`, the page now fetches that manifest **at build
time** and renders every captured screenshot — no manual curation:
- **before/after comparison sliders** for `changed` surfaces (e.g. the
xNet Cloud entry's Settings redesign, from PR #140)
- a **scroll-snap thumbnail strip** → native `<dialog>` **lightbox** for
`new`/curated images
- **click-to-play video** for interaction flows
- a **"View all N screenshots on PR #N"** link tying each entry to its
source

`status: "unchanged"` surfaces are dropped; a 404 (pre-0189 PR) or
offline build falls back to the curated `hero`/`images[]`.

## Interactive + richer
- **Tag-filter chips** + **client search** (combined), `?tag=` persisted
in the URL
- **Copy-link** button per entry; **GitHub author avatars**
- **LCP fix**: first hero is `eager`/`fetchpriority=high`, others `lazy`
- New `images`/`video`/`author` fields on `ChangelogEntry` (manual
override / fallback), surfaced in the JSON feed and covered by
`validate:changelog`
- New real entry: **xNet Cloud onboarding (#140)** — demonstrates a live
before/after slider

## Design choice: Option D (zero-dep)
Native `<dialog>` lightbox instead of PhotoSwipe; hand-rolled CSS
`input[type=range]` before/after slider instead of
`<img-comparison-slider>`; substring search instead of Fuse.js. Matches
the repo's zero-dep ethos — no lockfile/CDN/CSP surface, site stays
framework-free.

## Verified (build + headless browser)
- PR #140 entry renders a real before/after slider from its manifest ✓
- unchanged surfaces excluded; 404/offline falls back to hero ✓
- tag filter (`ai`→3), search (`dictation`→1), copy-link, lightbox
(correct image + "1/2"), slider `--cl-pos`→20% ✓
- `astro build` clean; JSON feed carries new fields ✓

Scope is **site-only** (no `apps/`/`packages/` changes), so the required
CI checks aren't affected. Deferred (noted in the doc): WebP/`srcset`,
in-app gallery rendering, formal axe/Lighthouse audit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
crs48 added a commit that referenced this pull request Jun 17, 2026
Implements exploration
[0197](docs/explorations/0197_[x]_AUTOMATED_CHANGELOG_ON_MERGE.md).
Fixes the root cause of "recent PRs aren't updating the changelog": it
was 100% manual (a hand-edited array + an AGENTS.md nudge). Now a merge
writes the entry itself.

## What

Three phases:

**Phase 1 — fragment-file storage (keystone).** Split the changelog
array into `site/src/data/changelog/<id>.json` fragments, loaded via
Vite `import.meta.glob`. One file per entry → CI appends on merge with
no array surgery and **no merge conflicts** between concurrent PRs. Pure
refactor: page + feeds render identically (13 items, the #140
before/after gallery intact).

**Phase 2 — the on-merge workflow.** `.github/workflows/changelog.yml`
triggers on `pull_request: closed` + `merged == true` (own-repo only,
skippable via `skip-changelog`). It runs
`scripts/changelog/from-pr.mjs`, which parses the PR body's `##
Changelog` block (or AI-drafts via `ANTHROPIC_API_KEY`), **stamps
id/date/PR/author from the event payload**, writes an idempotent
fragment, and commits to `main` with `RELEASE_GITHUB_TOKEN` so
`deploy-site` republishes. Block-less PRs get a `needs-changelog` label.

**Phase 3 — author ergonomics.** A `## Changelog` section in the PR
template + rewritten AGENTS.md: fill the PR-body block, don't edit data
by hand.

## Two load-bearing decisions
- **PAT, not `GITHUB_TOKEN`** — commits pushed with `GITHUB_TOKEN` don't
trigger downstream workflows, so the deploy would silently never run.
- **No `[skip ci]`** — the deploy *must* fire; no loop because the
trigger is the merge event, not a push.

## Verification (local)
- `from-pr.mjs`: block path → valid fragment (id `2026-06-18-pr999`,
tag-filtered); idempotent re-run → skip; no-block + no key → skip ✓
- `validate:changelog` accepts generated fragments; `astro build` clean
✓
- Workflow YAML parses; `skip-changelog`/`needs-changelog` labels
created ✓

Live end-to-end (a real merge writing a fragment + redeploy) happens on
the **next** PR after this one — the workflow has to be on `main` first.
I can follow up with a tiny test PR to confirm.

Scope is **site + CI + docs** only (no `apps/`/`packages/`), so required
checks aren't affected.

## Changelog

The changelog now updates itself when a PR merges
Write a short "## Changelog" note in your PR and it lands on the site
automatically — no more hand-editing.
- Entries are generated from the PR on merge, with the date, PR number,
and author filled in for you
- A `skip-changelog` label opts internal PRs out
tags: ci, platform

🤖 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.

2 participants