feat: xNet Cloud onboarding UI — marketing, signup→provision, claim flow (0192) - #140
Merged
Conversation
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>
Contributor
|
Preview removed for PR #140. |
Contributor
🖼️ UI changes in this PRScreensAuto-captured by CI · run. Informational — not a blocking check. |
| */ | ||
|
|
||
| /** 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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



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)
$0).xnet-cloudHono 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 — mirrorsPLAN_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
GET /auth/callback(seal an httpOnly signed session),POST /checkout,POST /webhook(verify → provision/suspend),POST /portal, and a server-rendered/dashboard.provisionForBillingprovisions the hub withdid=''(bound later via the claim flow);suspendTenant(cancel) retains the R2 replica;deleteTenant(delete data).TenantBillingGatewayport + keylessFakeTenantBillingGateway— 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/start+/device/token(app polls) and/claim(dashboard approval) → dual-proofbindDataIdentity. The app never embeds WorkOS.xnet:hub-urlsetting —App.tsxnow reads what Settings writes (lib/hub-url.ts).lib/cloud-claim.tsapp-side client; Settings → Network gains an "xNet Cloud" group (see plans / open dashboard).Verification
apps/cloud(signup funnel, device-grant claim, session sealing, signature verify) + 7apps/weblib.apps/cloud+apps/webtypecheck clean; eslint + prettier clean;astro buildgreen.Deferred (infra / not implementable in-repo) — see the doc's Implementation Status
Durable stores (still in-memory), real Stripe +
@xnetjs/identityadapters, 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-gatededitor-uxe2e.🤖 Generated with Claude Code