From 1a98ef8c8b6f001c27be8073e3c4e783ea8af453 Mon Sep 17 00:00:00 2001 From: Karan Date: Tue, 27 Oct 2026 18:43:10 -0400 Subject: [PATCH] telemetry-worker: /start/open-source, onboarding with no account and only the open-source parts A page at /start/open-source for teams that won't use a hosted dashboard: no sign-in, no token, telemetry: false. Checkboxes (Guard, Eager, Logs, the coding-agent link, a self-hosted model) rewrite one copy-prompt that installs the runtime, starts it with the discover import first, adds the dev-only overlay, connects the MCP server with GENCLASS_DASHBOARD and GENCLASS_REPORT unset, and checks pages with scan --local. The page lists every host the app or the dev machine still reaches (jsDelivr for the model and onnxruntime-web unless self-hosted, the npm registry, Playwright's download servers) and says the hosted page check is not open source and needs hosted: true. - choices come back after Back (Chromium restores checkboxes without a change event and skips disabled ones), here and in the dashboard's Connect dialog - copying by execCommand no longer scrolls the page and keeps focus on the button - prompt sources ship through fnSource(); bundle.test.ts bundles pages.ts as Wrangler does (keepNames) and fails on __name( in an inline script or a raw control character - the route writes no database rows; POST falls through to the site - SNIPPET_RUNTIME and RUNTIME_ON_NPM are 0.3.0-beta.4 - /start and the account pages link to it Co-Authored-By: Claude Opus 5.5 --- telemetry-worker/README.md | 7 +- telemetry-worker/src/account-pages.ts | 7 +- telemetry-worker/src/collector.ts | 14 +- telemetry-worker/src/pages.ts | 255 ++++++++++++++++++++++-- telemetry-worker/src/studio.ts | 11 +- telemetry-worker/test/auth.test.ts | 2 +- telemetry-worker/test/bundle.test.ts | 63 ++++++ telemetry-worker/test/dashboard.test.ts | 18 +- telemetry-worker/test/pages.test.ts | 250 ++++++++++++++++++++++- telemetry-worker/test/plans.test.ts | 4 +- telemetry-worker/wrangler.toml | 4 +- 11 files changed, 593 insertions(+), 42 deletions(-) create mode 100644 telemetry-worker/test/bundle.test.ts diff --git a/telemetry-worker/README.md b/telemetry-worker/README.md index 6702f45a..e5fdab9a 100644 --- a/telemetry-worker/README.md +++ b/telemetry-worker/README.md @@ -46,9 +46,9 @@ genclass.dev/dashboard/ --> dashboard page --GET /api/projects/? | file | what | |---|---| | `src/index.ts` | entry point: default export only (`fetch`, `scheduled`). The Workers runtime treats every named export of the main module as an entrypoint, so nothing else is exported here. | -| `src/collector.ts` | ingest (`POST /v1/events`), envelope validation (incl. optional `token`), routing for `/api/*`, `/start`, `/dashboard*`; hands account paths to `src/auth.ts` | +| `src/collector.ts` | ingest (`POST /v1/events`), envelope validation (incl. optional `token`), routing for `/api/*`, `/start`, `/start/open-source`, `/dashboard*`; hands account paths to `src/auth.ts` | | `src/dashboard.ts` | ids and hashing, project creation and lookup, token cache, aggregation of a batch into counters, stats for the dashboard, retention | -| `src/pages.ts` | HTML/CSS/JS for `/start` and `/dashboard/`, and the shell and dashboard the account pages reuse (`/account/apps/` is the dashboard in its account mode); the dashboard's views, its Connect dialog (Framework, API Keys, MCP, Analytics), the MCP view and the agent link box, Copy prompt and Copy as Markdown; inline, per-response CSP nonce, fonts from Google Fonts only | +| `src/pages.ts` | HTML/CSS/JS for `/start`, `/start/open-source` and `/dashboard/`, and the shell and dashboard the account pages reuse (`/account/apps/` is the dashboard in its account mode); the dashboard's views, its Connect dialog (Framework, API Keys, MCP, Analytics), the MCP view and the agent link box, Copy prompt and Copy as Markdown; inline, per-response CSP nonce, fonts from Google Fonts only | | `src/studio.ts` | the Supabase Studio frame the dashboards and the organization pages use: the 48px header (organization and project switchers, Connect, Feedback, search, help, the account menu), the icon rail that widens on hover, product menus, the dialog and tabs, the command menu (Ctrl/Cmd K), Studio's dark tokens and icons; the copy helpers every page shares | | `src/escape.ts` | `esc` and `embed`, for every page module | | `src/auth.ts` | accounts: Google, email and password, GitHub, passkeys (sign-in, the passkey step, fresh confirmations), sessions, CSRF, the device flow, owned apps, their routes and their retention | @@ -80,6 +80,7 @@ genclass.dev/dashboard/ --> dashboard page --GET /api/projects/? | `GET /api/projects/?range=24h\|7d\|30d` | dashboard stats JSON (every plan includes the dashboard; a plan file that left it out would answer 402 `{ ok:false, error, locked:true }`) (default `7d`): `name, token, created, lastEvent, range, from, generated, totals, breakdowns{metric: [{key,count}]}` (top 25 each; 200 for `detect_issue` and `possible_issue`), `distinct{metric: n}` (distinct keys before that cut, e.g. pages with a problem), `series` (24 hourly or 7/30 daily buckets: sessions, decisions, detections, acted, possible, ix_views, ix_sessions), `recent` (last 50 detections/interventions in the window; possible ones are not listed). `totals` also has `latencyAvgMs`, `modelLoadAvgMs`, `failOpens`, `modelErrors`, `interventions`, `actionLimits`, `detectionsWithPage` and `possibleWithPage`. Apps that turned on interaction analytics (runtime `interactions` events, [docs/runtime/ANALYTICS.md](../docs/runtime/ANALYTICS.md)) also get `ix_views`, `ix_engaged_ms`, `ix_click`, `ix_rage` and `ix_dead` totals and breakdowns (by route, or route TAB element key; `ix_scroll` by route TAB milestone; top 200 each), counted with their own per-batch budget; and per visit `ix_sessions`, `ix_ref`, `ix_utm_source`, `ix_utm_medium`, `ix_utm_campaign`, `ix_device`, `ix_browser`, `ix_os`, `ix_country`, `ix_returning`, plus `visits` (`sessions, pages, bounces, entry[], exit[]` over the visits that started in the period) and `funnels` (`[{ name, steps, counts }]`; docs/runtime/ANALYTICS.md section 3.1); with navigation paths (ANALYTICS.md section 11) also `ix_entry` and `ix_exit` (totals and by route), `ix_nav` and `ix_nav_ms` (from TAB to), `ix_arrive` (by `nav`, and route TAB `nav`), `ix_first` (route TAB element key), `ix_first_n` and `ix_first_ms` (by route) and `ix_path` (the first 5 routes of a page load, joined with ` > `). 404 `{ ok:false, error:"not found" }` for an unknown or malformed secret. Same-origin only (no CORS). | | `PUT /api/projects//funnels` | body `{ funnels: [{ name, steps }] }` (application/json, at most 4 KB): replaces the app's funnels (at most 5, 2 to 6 routes each; a step ending in `*` matches every route under it). 200 `{ ok, funnels }`, 400 with the reason, 403 for the sample. Account mode: `PUT /api/account/projects//funnels` with the browser session and the site's Origin | | `GET /start` | "Get a token" page | +| `GET /start/open-source` | "Use GenClass without an account": the open-source parts only (the runtime, the agent link, the MCP server and the local page check, all Apache-2.0), with no account, no token and `telemetry: false`, so nothing reaches genclass.dev; for teams that cannot sign up for or send data to a hosted dashboard. Choices (Guard and Eager, Findings in our own logs: the `console-json` sink, Serve the model ourselves: `fetch-model`, Deploy when done) rewrite one copied prompt (`openSourcePrompt`, run from its own source in the page, as `installPrompt` is); the page sends no request. Linked from `/start` and under the sign-in ways on `/login` and `/signup` | | `POST /api/projects//agent` | the MCP server's log of the closed loop, mirrored, when the developer sets `GENCLASS_REPORT` for `@genclass/mcp` (docs/runtime/AGENT.md section 4.5): `{ schema: "genclass-agent/1", events: [{ t: "found" \| "explain" \| "armed" \| "replay" \| "test" \| "explore" \| "cleared", at, id?, kind?, route?, reproduced?, runs?, races?, what? }] }`, at most 50 events and 16 KB a request, rate limited per token (`SESSION_LIMIT`). Every accepted event is one row of `agent_log` (at most 500 per app, the oldest go first; unknown types are dropped); the ones about a finding also update `agent_reports` (kind, normalised route, found, explained, last replay and verdict, how many replays reproduced it, tested, proven: a replay that stops reproducing after one that did). 90 days; the stats carry them as `agent` (`findings`, `counts`, `stages`: found, explained, reproduced, fixed, closed, open, continued; `log`, the last 50) and the dashboard's Closed-loop development view shows them. The sample and unknown secrets 404; other methods 405. Ids, kinds, routes, verdicts, counts and times only, never page text | | `GET /dashboard/` | the dashboard (for an app with no account too: it is on Free), in the Supabase Studio frame (`src/studio.ts`), in views the rail switches between: Project overview (the name and status, the summary, four stats, the problems chart, usage cards per hour or day, Get connected, the problems that need attention with a Copy prompt each and Fix them all, and Getting started until the first event), Problems (each kind with its pages, its fix and a Copy prompt for a coding agent; Fix them all copies one prompt for every kind and the dev page's checks, in which the agent says what to do on the page and waits for the loads itself), Pages and Actions, Activity, Visitors (without interaction data, under the copy-prompt button, the runtime analytics need, `ANALYTICS_RUNTIME` 0.3.0-beta.1, with the install line and the app's older versions from the `runtime` breakdown; the Connect dialog says the same), Health, MCP (the @genclass/mcp setup per coding agent, with GENCLASS_DASHBOARD set to the agent link, a Copy prompt, the tools, the JSON address), Closed loop (Closed-loop development: Fig. 3, every issue's path, found, reproduced, fix proven, closed, or round again, with the period's counts from the agent's reports; Table 15, the agent's log mirrored from `@genclass/mcp`; Fig. 6, the app's problems and what the agent does with each), Integrations (forwarding, account mode) and Project Settings (general, the key, agent access; Export in account mode). Problems, Visitors and Project Settings have Studio's product menu. The header's Connect opens the Connect dialog (also `#connect`); its Framework tab's prompt (`installPrompt`: the runtime, or the script tags of a site with no build step, the state in one atom per response, the agent link, the MCP server) follows the choices above it, Guard (and Eager), Badge on the live site, Connect my coding agent to production (makes the agent link, or names a private link) and Deploy when done, by running `installPrompt`'s own source in the page; the hash names the view (`#found`, `#recent-s`, `#mcp`); every data view copies as Markdown; and without the script every section shows. On a private link the page's own address is the agent link. With a plan file that left the dashboard out, the locked page (sign in, add the app to the account, see the plans; `src/locked.ts`). 404 page for an unknown secret; `GET /dashboard` redirects to `/start`. `/dashboard/sample` stays open | @@ -407,7 +408,7 @@ The daily cron also deletes expired challenges and device codes and expired or r |---|---|---|---| | `AUTH_ORIGIN` | var | `https://genclass.dev` | expected `Origin`, WebAuthn origin, OAuth redirect URIs, cookie `Secure` | | `AUTH_RP_ID` | var | `genclass.dev` | WebAuthn RP ID | -| `RUNTIME_ON_NPM` | var (wrangler.toml) | `0.3.0-beta.3` | the newest `@genclass/runtime` on npm, on any dist-tag; below `0.3.0-beta.0` the Connect dialog, `/start` and private dashboards say first that keys and `login` are not published yet. Set it with `src/pages.ts` `SNIPPET_RUNTIME` to every release once release.yml has published it and jsDelivr serves it, then deploy; at `0.3.0` also set `RUNTIME_PKG` (the snippets pin the script tag to `SNIPPET_RUNTIME` and name the `@beta` dist-tag in their commands while it is a prerelease) | +| `RUNTIME_ON_NPM` | var (wrangler.toml) | `0.3.0-beta.4` | the newest `@genclass/runtime` on npm, on any dist-tag; below `0.3.0-beta.0` the Connect dialog, `/start` and private dashboards say first that keys and `login` are not published yet. Set it with `src/pages.ts` `SNIPPET_RUNTIME` to every release once release.yml has published it and jsDelivr serves it, then deploy; at `0.3.0` also set `RUNTIME_PKG` (the snippets pin the script tag to `SNIPPET_RUNTIME` and name the `@beta` dist-tag in their commands while it is a prerelease) | | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` | secret | unset | Google sign-in, the first way in; both needed. An email and a password are offered either way | | `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` | secret | unset | GitHub sign-in; both needed | diff --git a/telemetry-worker/src/account-pages.ts b/telemetry-worker/src/account-pages.ts index 53f7c6ef..455beb95 100644 --- a/telemetry-worker/src/account-pages.ts +++ b/telemetry-worker/src/account-pages.ts @@ -272,9 +272,12 @@ export interface AuthPageOptions { notice?: string | null; } -/** Under the sign-in ways: the runtime needs no account, and what an account's Free plan gives (billing/plans.json). */ +/** + * Under the sign-in ways: the runtime needs no account (the way in without one: /start/open-source, pages.ts + * openSourcePage), and what an account's Free plan gives (billing/plans.json). + */ const optional = (): string => - `

The runtime needs no account. ${entitled(FREE, "dashboard") ? "The dashboard is free for every app." : "Dashboards come with a paid plan."}

`; + `

The runtime needs no account: set it up without one. ${entitled(FREE, "dashboard") ? "The dashboard is free for every app." : "Dashboards come with a paid plan."}

`; /** The passkey sign-in button's script: sign in, then go to `next` (or reload, on the device page). */ const PASSKEY_LOGIN_JS = ` diff --git a/telemetry-worker/src/collector.ts b/telemetry-worker/src/collector.ts index 86e1c83d..525d179a 100644 --- a/telemetry-worker/src/collector.ts +++ b/telemetry-worker/src/collector.ts @@ -11,8 +11,9 @@ // OPTIONS /v1/events (CORS preflight), GET /v1/health. // // Per-app tokens and dashboards (src/dashboard.ts, src/pages.ts; also routed on genclass.dev/{api/*,start*,dashboard*}): -// POST /api/projects, GET /api/projects/, GET /start, GET /dashboard/. A batch with a known top-level -// `token` is also aggregated into D1 for that app's dashboard; without one it is stored in R2 only, exactly as before. +// POST /api/projects, GET /api/projects/, GET /start, GET /start/open-source (no account, no token), GET +// /dashboard/. A batch with a known top-level `token` is also aggregated into D1 for that app's dashboard; +// without one it is stored in R2 only, exactly as before. // Every plan includes the dashboard (billing/plans.json), so a private link opens it, also for an app with no account // (Free). A plan file that left the dashboard out would open the locked page instead (src/locked.ts) and its stats // would answer 402. Collection never depends on it: every known token is aggregated. @@ -42,7 +43,7 @@ import { import { routeBilling } from "./billing.js"; import { json, readCapped } from "./http.js"; import { afterBatch } from "./paid.js"; -import { dashboardPage, notFoundPage, ownEndpoint, securityHeaders, startPage } from "./pages.js"; +import { dashboardPage, notFoundPage, openSourcePage, ownEndpoint, securityHeaders, startPage } from "./pages.js"; import { lockedLink } from "./locked.js"; import { hasDashboard, projectAccess } from "./plans.js"; import { SAMPLE_PROJECT, SAMPLE_SECRET, sampleStats } from "./sample.js"; @@ -406,6 +407,13 @@ async function routeSite(request: Request, url: URL, path: string, env: Env, opt return html(200, startPage(nonce, { runtimeReady: runtimeHasTokens(env), endpoint: ownEndpoint(env) }), nonce); } + // the open-source parts with no account and no token (pages.ts openSourcePage): static, nothing stored + if (path === "/start/open-source") { + if (m !== "GET" && m !== "HEAD") return null; + const nonce = randomBase62(22); + return html(200, openSourcePage(nonce), nonce); + } + if (path === "/dashboard") { if (m !== "GET" && m !== "HEAD") return null; return new Response(null, { status: 302, headers: { location: "/start", "cache-control": "no-store" } }); diff --git a/telemetry-worker/src/pages.ts b/telemetry-worker/src/pages.ts index dd60f1ec..d1e44caa 100644 --- a/telemetry-worker/src/pages.ts +++ b/telemetry-worker/src/pages.ts @@ -1,5 +1,5 @@ -// HTML for https://genclass.dev/start (get a token) and https://genclass.dev/dashboard/ (the dashboard), and -// the shell and dashboard the account pages (src/account-pages.ts) reuse: /account/apps/ is this dashboard in its +// HTML for https://genclass.dev/start (get a token), /start/open-source (the open-source parts, no account) and +// https://genclass.dev/dashboard/ (the dashboard), and the shell and dashboard the account pages (src/account-pages.ts) reuse: /account/apps/ is this dashboard in its // account mode (the account's projects in the header's switcher, the Connect dialog's ways in, the agent link, stats // from the account API). Every plan includes the dashboard (billing/plans.json); a plan file that left it out would // give the locked account mode (the key and the Connect dialog, the plans, no stats), and a locked private link is @@ -25,7 +25,7 @@ export const ANALYTICS_RUNTIME = "0.3.0-beta.1"; * set SNIPPET_RUNTIME to "0.3.0" and RUNTIME_PKG to "@genclass/runtime" (and RUNTIME_ON_NPM in wrangler.toml), then * deploy. */ -export const SNIPPET_RUNTIME = "0.3.0-beta.3"; +export const SNIPPET_RUNTIME = "0.3.0-beta.4"; export const RUNTIME_PKG = "@genclass/runtime@beta"; export const CDN_SCRIPT = `https://cdn.jsdelivr.net/npm/@genclass/runtime@${SNIPPET_RUNTIME}`; /** What local and private hosts do, as the runtime behaves (src/telemetry/config.ts) and PRIVACY.md says. */ @@ -293,7 +293,8 @@ export function startPage(nonce: string, opts: { runtimeReady?: boolean; endpoin

Get a token for your web app

One token per app. Add it to GenClass and every page load, detection and fix is collected for the app's private dashboard. The dashboard is free.

-

To keep several apps in one place, create an account or sign in.

+

To keep several apps in one place, create an account or sign in. + No account and no token: use the open-source parts only.

@@ -346,6 +347,224 @@ See return shell(nonce, "Get a token | GenClass", body, script); } +// ------------------------------------------------------------------------------------- /start/open-source +// GenClass's open-source parts with no account: the runtime in the app's pages, the agent link, the MCP server and the +// page check, all Apache-2.0, with no token and the runtime's anonymous diagnostics off, so nothing reaches +// genclass.dev. For teams that cannot sign up for or send data to a hosted dashboard (enterprises). The page sends +// nothing either: its choices rewrite the prompt in the browser, with the server's own openSourcePrompt. + +/** What /start/open-source's choices add to openSourcePrompt; all off is observe, the overlay in development only. */ +export interface OpenSourceChoices { + /** guard: GenClass may act when it is sure (drop a stale response, fix a CSS bug on the page), reversibly */ + guard?: boolean; + /** with guard: act on smaller signals (aggressiveness "eager") */ + eager?: boolean; + /** the console-json sink: every record also as one JSON line on the console, for the app's own log shipper */ + logs?: boolean; + /** the model and onnxruntime-web from the app's own host (`fetch-model`), not jsDelivr */ + selfHost?: boolean; + /** deploy with the project's own deploy command at the end */ + deploy?: boolean; +} + +/** + * The prompt /start/open-source copies: installPrompt's steps without a token, a dashboard or an agent link, with + * `telemetry: false` (the runtime sends no data anywhere; it still downloads public files from jsDelivr unless the app + * serves them), the MCP server on the developer's machine and the page check run locally. Pure and self-contained, + * like installPrompt: the page script runs this same function. + */ +export function openSourcePrompt(c: OpenSourceChoices = {}, n: PromptNames = PROMPT_NAMES): string { + const guard = !!c.guard; + const eager = guard && !!c.eager; + const logs = !!c.logs; + const own = !!c.selfHost; + const dir = "/genclass-model/"; + // GenClass.init has automatic state discovery off unless asked (autoState: true, with the discover import first) + const init = ["telemetry: false", "autoState: true"]; + if (guard) init.push('mode: "guard"', ...(eager ? ['aggressiveness: "eager"'] : []), "visual: { on: true, fix: true }"); + if (own) init.push(`model: { baseUrl: "${dir}", ortWasmPaths: "${dir}ort/" }`); + if (logs) init.push("sinks: [consoleJson()]"); + // the script tag carries all of it but the aggressiveness, which goes in GENCLASS_CONFIG (as in installPrompt) + const attrs = ['data-telemetry="off"']; + if (guard) attrs.push('data-mode="guard"', 'data-visual="fix"'); + if (own) attrs.push(`data-model="${dir}"`, `data-ort="${dir}ort/"`); + if (logs) attrs.push('data-sink="console-json"'); + attrs.push('data-devtools="local"'); + const flags = ["--no-telemetry", ...(guard ? ["--mode guard"] : []), ...(logs ? ["--sink console-json"] : []), ...(own ? [`--model-url ${dir}`] : [])]; + // init writes no visual or aggressiveness option: the prompt says where to add them + const initAdds = guard + ? `${eager ? 'aggressiveness: "eager" and ' : ""}visual: { on: true, fix: true } to the option file it writes (data-visual="fix" on the tag it writes${eager ? ", and the GENCLASS_CONFIG line above" : ""}; Astro gets a line instead: add visual=fix to its content${eager ? ', and the aggressiveness in a window.GENCLASS_CONFIG script before it' : ""})` + : ""; + const local = '["localhost", "127.0.0.1"].includes(location.hostname)'; + // A step is a one-element array, numbered at the end (choices add steps); other lines are strings. No named inner + // function: Wrangler's bundler (keepNames) would wrap it in a __name() helper that the page script, which runs this + // function's source, lacks. + const out: (string | [string])[] = [ + "Add GenClass's open-source parts to this web app (Apache-2.0, https://github.com/genclass-dev/GenClass-lib): the @genclass/runtime library in its pages, which reports the bugs that never throw (double submits, stale responses that overwrite newer data, write races, visual bugs), and the @genclass/mcp server on this machine, so you, the coding agent, can see what happens on the dev pages, replay it and prove fixes. No GenClass account and no token: nothing is sent to genclass.dev. Do every step yourself.", + "", + [`Install the runtime with the project's package manager: npm install ${n.pkg} (or pnpm add / yarn add / bun add ${n.pkg}). A site with no build step (plain HTML pages, no bundler) installs nothing: it uses the script tags below.`], + [ + "Start it as early as possible, with GenClass's anonymous diagnostics off (telemetry: false), so it sends no data anywhere" + + (own ? "" : "; the browser still downloads its model and onnxruntime-web, public files, from cdn.jsdelivr.net (about 13 MB, 18 MB on WebGPU devices, then cached) unless the app serves them") + + ". With a bundler, in the browser entry (main.ts, the root layout or the root component), with the discover import first, before React, Redux or Zustand load:", + ], + ' import "@genclass/runtime/discover"; // first: lets GenClass find React, Redux and Zustand state', + ...(logs ? [' import { consoleJson } from "@genclass/runtime/sinks/console-json";'] : []), + ' import { GenClass } from "@genclass/runtime";', + ` const rt = GenClass.init({ ${init.join(", ")} });`, + " Without a bundler, in the of every HTML page, before the page's own scripts:", + ...(eager ? [' '] : []), + ` `, + " and in the page's scripts the handle (rt below) is GenClass.runtime.", + ` (Or npx ${n.pkg} init ${flags.join(" ")} --yes does steps 1 and 2 and shows the diff, with a bundler or plain HTML pages: it starts GenClass with its zero-code entry (the /auto import with an option file, or the script tag pinned with an integrity hash), which finds React, Redux and Zustand state by itself, and adds step 3's dev-only overlay; the handle is then GenClass.runtime${guard ? `; add ${initAdds}` : ""}${own ? "; run the fetch-model command below before it" : ""}.)`, + [ + (guard + ? `Mode guard: when GenClass is sure, it acts with minimal, reversible steps (it drops a stale response or fixes a CSS bug on the page) and never changes a response${eager ? '; aggressiveness "eager" lets it act on smaller signals' : ""}.` + : "Keep the default mode, observe: GenClass reports and never changes what the app does.") + + ' In development builds also mount the devtools overlay, which shows what GenClass sees on the page: if (import.meta.env.DEV) import("@genclass/runtime/devtools").then((d) => d.mountDevtools(rt)); (Next.js: process.env.NODE_ENV === "development"; the script tag\'s data-devtools="local" does the same).', + ], + ['Connect the state that responses write (search results, carts, lists, forms), so GenClass sees each write with the response or click that caused it. React state, Redux stores (configureStore, or createStore with the Redux DevTools compose) and Zustand stores made with the devtools middleware are found automatically (the discover import with autoState: true above; the script tag and init\'s /auto import do it by default); otherwise create it with rt.atom("name", initial) or wrap the store you have with rt.guard("name", { get, set }), and write through the handle. Values one response writes together (a search\'s query and its results) go in one atom, written once per response, so GenClass can drop a stale response as one write: const search = rt.atom("search", { query: "", results: [] }); then search.set((s) => ({ ...s, query: data.q, results: data.results })) and render from search.subscribe.'], + ['Development builds only, add the agent link: import { connectAgent } from "@genclass/runtime/agent"; if (import.meta.env.DEV) connectAgent(rt); (Next.js: process.env.NODE_ENV === "development"). It records what happens on local pages for the GenClass MCP server, talks only to 127.0.0.1 and is never part of a production build.'], + ` Without a bundler, after the runtime's script tag: `, + [`Add the GenClass MCP server to yourself: npx -y ${n.mcp} init writes .mcp.json for Claude Code (npx -y ${n.mcp} config prints the block for Cursor, VS Code, Codex, Gemini CLI and others); then reload your MCP servers. It runs on this machine, its bridge listens on 127.0.0.1 only, and it needs no account: leave GENCLASS_DASHBOARD and GENCLASS_REPORT unset (either one sends to a GenClass dashboard). init also prints setup lines for the app with a bare GenClass.init(): keep the setup from steps 2 and 5 instead. Its tools: genclass_status, genclass_findings, genclass_explain, genclass_replay, genclass_explore, genclass_scan.`], + ...(logs + ? [ + ['Findings print as [GenClass] lines in the browser console, and the console-json sink also prints each one as one JSON line, for the log shipper the app already uses. If the team has its own endpoint for them, the webhook sink sends batched POSTs there instead: import { webhook } from "@genclass/runtime/sinks/webhook"; sinks: [webhook({ url: "" })], requests: { ignore: [""] } (so GenClass does not watch its own POSTs; data-sink="webhook:" on the script tag does both). Ask me for that URL rather than inventing one.'] as [string], + ] + : []), + ...(own + ? [ + [`Serve the model from the app itself, so its pages download no model from a CDN: npx ${n.pkg} fetch-model public/genclass-model (the folder the project serves as-is: public/ for Vite, Next.js and Astro, static/ for SvelteKit), before init if you use init. It downloads the model and onnxruntime-web's files, checks the model files against the sizes and SHA-256 hashes its model card lists, and records onnxruntime-web's hashes in ort/ort.json; the model option above points at it. On a host with a 25 MiB per-file limit (Cloudflare), run fetch-model public/genclass-model --variant q8 --ort wasm instead and add device: "wasm" to the model option (data-device="wasm" on the script tag; init --model-url adds it when it finds only the WASM build). With a bundler nothing else loads from a CDN. Without one, the runtime's script and what it loads on demand (its model worker, onnxruntime-web's JavaScript, sinks, visual checks, the overlay, the agent link) still come from jsDelivr unless you serve the package's dist/ folder from the app and point the script tag and the agent link's import at it (data-base if they are in different folders).`] as [string], + ] + : []), + [`Run the dev server and load a page to check that the app builds and runs without errors. Until the MCP server runs (your next conversation), the page logs a failed request to 127.0.0.1:47357: that is the agent link finding no server, and expected. In your next conversation, reload a dev page and genclass_status lists it. Then check a page for visual bugs with genclass_scan on its local URL, or from a terminal: npx ${n.pkg} scan http://localhost:5173/ --local (keep --local). It runs headless Chromium on this machine and needs Playwright: if the scan says it is missing, ask me before adding playwright as a dev dependency and downloading Chromium (about 600 MB): npm i -D playwright && npx playwright install chromium; or skip the scan and say so. Do not use hosted: true: it sends the page URL to GenClass's hosted page check, which is not open source.`], + ...(c.deploy + ? [["Deploy it, as chosen, with the command this project already uses (a deploy script in package.json, wrangler deploy for a Cloudflare Worker, vercel deploy --prod, and so on), then check that the deployed site serves the change."] as [string]] + : []), + [`Tell me what you changed${c.deploy ? " and where it is deployed" : ""}, that the app sets no GenClass token and has telemetry off, what it downloads from outside this network, and that a new conversation with you has the GenClass tools.`], + ]; + let k = 0; + return out.map((l) => (typeof l === "string" ? l : `${++k}. ${l[0]}`)).join("\n"); +} + +/** A function's own source for a page script, safe inside its `, link ? `6. Add the GenClass MCP server to yourself with this project's agent link, in your own settings rather than a file in the repository (anyone with the link can read this app's stats). Claude Code: claude mcp add genclass -e GENCLASS_DASHBOARD=${link} -e GENCLASS_REPORT=on -- npx -y ${n.mcp} (other agents: npx -y ${n.mcp} config prints the block; add those two env values to it). GENCLASS_REPORT=on mirrors what you find, replay and prove to the dashboard's Loop view. Its tools: genclass_status, genclass_findings, genclass_explain, genclass_replay, genclass_explore, genclass_scan, genclass_analytics.` : `6. Add the GenClass MCP server to yourself: npx -y ${n.mcp} init writes .mcp.json for Claude Code (npx -y ${n.mcp} config prints the block for Cursor, VS Code, Codex, Gemini CLI and others); then reload your MCP servers. Its tools: genclass_status, genclass_findings, genclass_explain, genclass_replay, genclass_explore, genclass_scan.`, - "7. Run the dev server and load a page to check that the app builds and runs without errors; genclass_status should then list the page (a new MCP server loads in your next conversation). Local hosts (localhost, private networks) send nothing to the dashboard by default, so the first production events come from the deployed site.", + "7. Run the dev server and load a page to check that the app builds and runs without errors; genclass_status lists a dev page once you load it with the MCP server running (a new MCP server loads in your next conversation). Local hosts (localhost, private networks) send nothing to the dashboard by default, so the first production events come from the deployed site.", ...(c.deploy ? ["8. Deploy it, as chosen on the dashboard, with the command this project already uses (a deploy script in package.json, wrangler deploy for a Cloudflare Worker, vercel deploy --prod, and so on), then check that the deployed site serves the change."] : []), @@ -2052,11 +2275,6 @@ export function installPrompt(token: string, endpoint: string | null = null, c: return out.join("\n"); } -/** installPrompt's own source for the page script, safe inside its `); expect(p).toContain("observe"); expect(p).toContain("Run the dev server"); @@ -884,9 +890,9 @@ describe("dashboard page: the Supabase Studio frame (src/studio.ts): header, rai expect(p).toContain('import { connectAgent } from "@genclass/runtime/agent"; if (import.meta.env.DEV) connectAgent(rt);'); expect(p).toContain(`npx -y ${MCP_PKG} init`); expect(p).toContain('import("@genclass/runtime/devtools").then((d) => d.mountDevtools(rt))'); - expect(p).toContain("genclass_status should then list the page"); + expect(p).toContain("genclass_status lists a dev page once you load it with the MCP server running"); expect(p).toContain(`npx ${RUNTIME_PKG} init --token gc_TestToken0123456789abc --yes`); - expect(installPrompt("gc_x", "https://c.example/v1/events")).toContain('GenClass.init({ token: "gc_x", telemetry: { endpoint: "https://c.example/v1/events" } });'); + expect(installPrompt("gc_x", "https://c.example/v1/events")).toContain('GenClass.init({ token: "gc_x", autoState: true, telemetry: { endpoint: "https://c.example/v1/events" } });'); // a site with no build step gets everything from the prompt alone: the script tag, the agent link from the CDN on local hosts expect(p).toContain("A site with no build step (plain HTML pages, no bundler) installs nothing"); expect(p).toContain(`import("${CDN_SCRIPT}/dist/agent.js")`); @@ -899,7 +905,7 @@ describe("dashboard page: the Supabase Studio frame (src/studio.ts): header, rai it("Connect prompt choices: guard (and eager), the badge on every host, the agent link with the Loop view, deploy", () => { const t = "gc_TestToken0123456789abc"; const g = installPrompt(t, null, { guard: true, eager: true, badge: true, deploy: true, agentLink: "https://genclass.dev/dashboard/LINK" }); - expect(g).toContain(`const rt = GenClass.init({ token: "${t}", mode: "guard", aggressiveness: "eager", visual: { on: true, fix: true } });`); + expect(g).toContain(`const rt = GenClass.init({ token: "${t}", autoState: true, mode: "guard", aggressiveness: "eager", visual: { on: true, fix: true } });`); expect(g).toContain(''); expect(g).toContain(``); expect(g).toContain("3. Mode guard, as chosen on the dashboard"); @@ -1040,6 +1046,29 @@ describe("dashboard page: the Supabase Studio frame (src/studio.ts): header, rai await s.close(); }); + it("Connect choices a browser brings back with the page (back, reload; no change event) rewrite the prompt on pageshow", async () => { + const st = sampleStats("7d", NOW); + const r = await render(st, { account, id: "0123456789abcdef", firstEvent: "2026-10-12T09:00:00.000Z", runtimeReady: true }); + const box = (k: string) => r.doc.querySelector(`#cchoices [data-choice="${k}"]`) as unknown as HTMLInputElement; + const copied = () => r.doc.getElementById("cpinstall")!.getAttribute("data-copy"); + box("guard").checked = true; + box("badge").checked = true; + expect(copied()).toBe(installPrompt(st.token)); + r.win.dispatchEvent(new r.win.Event("pageshow")); + expect(copied()).toBe(installPrompt(st.token, null, { guard: true, badge: true })); + expect(box("eager").disabled).toBe(false); + // a restored eager without guard, and a restored agent box with no link yet, are unticked; nothing is created + box("guard").checked = false; + box("eager").checked = true; + box("agent").checked = true; + r.win.dispatchEvent(new r.win.Event("pageshow")); + expect(box("eager").checked).toBe(false); + expect(box("agent").checked).toBe(false); + expect(copied()).toBe(installPrompt(st.token, null, { badge: true })); + expect(r.writes).toEqual([]); + await r.close(); + }); + it("Delete project in an account: Project Settings' danger zone asks once, then sends DELETE with the confirm; a private link has none", async () => { const r = await render(sampleStats("7d", NOW), { account, id: "0123456789abcdef", firstEvent: "2026-10-12T09:00:00.000Z", runtimeReady: true }); expect([...r.doc.querySelectorAll(".sb-pm a")].some((a) => a.getAttribute("href") === "#delproj" && a.textContent === "Delete project")).toBe(true); @@ -1090,3 +1119,212 @@ describe("dashboard page: the Supabase Studio frame (src/studio.ts): header, rai } }); }); + +describe("/start/open-source: the open-source parts with no account, no token and nothing sent to genclass.dev", () => { + /** The page in happy-dom with its own script run; `fetch` records any request (there must be none). */ + async function openSource(o: { refuse?: boolean; before?: (doc: Document) => void; pageshow?: boolean } = {}) { + const html = openSourcePage("NONCE"); + const { body, script } = page(html); + const win = new Window({ url: "https://genclass.dev/start/open-source" }); + const doc = win.document; + doc.body.innerHTML = body; + o.before?.(doc as unknown as Document); + const urls: string[] = [], copied: string[] = []; + // a clipboard that records what was copied, or one the browser refuses + const nav = { clipboard: { writeText: (t: string) => (o.refuse ? Promise.reject(new Error("denied")) : Promise.resolve(void copied.push(t))) } }; + const run = new Function("window", "document", "location", "fetch", "navigator", script); + run(win, doc, win.location, async (u: string) => void urls.push(u), nav); + // a browser fires pageshow after load (and after it brings ticked boxes back); pageshow: false stops before it + if (o.pageshow !== false) win.dispatchEvent(new win.Event("pageshow")); + const box = (k: string) => doc.querySelector(`#choices [data-choice="${k}"]`) as unknown as HTMLInputElement; + const copy = () => doc.getElementById("cp")!.getAttribute("data-copy"); + return { html, win, doc, urls, copied, box, copy, close: () => win.happyDOM.close() }; + } + + it("the default prompt: no token, telemetry off, observe, the agent link and the MCP server without a dashboard, the local scan", () => { + const p = openSourcePrompt(); + expect(p).toContain("No GenClass account and no token: nothing is sent to genclass.dev."); + expect(p).toContain(`npm install ${RUNTIME_PKG}`); + expect(p).toContain("const rt = GenClass.init({ telemetry: false, autoState: true });"); + // GenClass.init finds React, Redux and Zustand state only with the discover import first and autoState: true + expect(p).toContain('import "@genclass/runtime/discover"; // first'); + expect(p.indexOf('import "@genclass/runtime/discover"')).toBeLessThan(p.indexOf('import { GenClass } from "@genclass/runtime"')); + // no data is sent, but the public files still come from jsDelivr unless the app serves them; the prompt says so + expect(p).toContain("so it sends no data anywhere; the browser still downloads its model and onnxruntime-web, public files, from cdn.jsdelivr.net"); + expect(p).not.toContain("sends nothing anywhere"); + expect(p).toContain(``); + expect(p).toContain(`npx ${RUNTIME_PKG} init --no-telemetry --yes does steps 1 and 2`); + expect(p).toContain("and adds step 3's dev-only overlay; the handle is then GenClass.runtime.)"); + expect(p).toContain("Keep the default mode, observe"); + expect(p).toContain('rt.atom("search", { query: "", results: [] })'); + expect(p).toContain('import { connectAgent } from "@genclass/runtime/agent"; if (import.meta.env.DEV) connectAgent(rt);'); + expect(p).toContain(`import("${CDN_SCRIPT}/dist/agent.js")`); + expect(p).toContain(`npx -y ${MCP_PKG} init writes .mcp.json`); + expect(p).toContain("leave GENCLASS_DASHBOARD and GENCLASS_REPORT unset"); + // the MCP server's init prints its own setup lines (a bare GenClass.init()): the prompt keeps its own + expect(p).toContain("keep the setup from steps 2 and 5 instead"); + expect(p).toContain(`npx ${RUNTIME_PKG} scan http://localhost:5173/ --local`); + expect(p).toContain("Do not use hosted: true: it sends the page URL to GenClass's hosted page check, which is not open source."); + // the scan needs Playwright: the agent asks before adding the dev dependency and downloading Chromium + expect(p).toContain("ask me before adding playwright as a dev dependency and downloading Chromium (about 600 MB): npm i -D playwright && npx playwright install chromium"); + // the agent link's failed request before the MCP server runs is expected + expect(p).toContain("the page logs a failed request to 127.0.0.1:47357"); + expect(p).toContain("In your next conversation, reload a dev page and genclass_status lists it."); + // discovery covers React state, Redux through its DevTools hooks and Zustand with devtools; the prompt says so + expect(p).toContain("Zustand stores made with the devtools middleware are found automatically"); + expect(p).toContain("(about 13 MB, 18 MB on WebGPU devices, then cached)"); + // nothing that needs an account, a token or the hosted dashboard + expect(p).not.toMatch(/token: "|data-token|--token|gc_[A-Za-z0-9]/); + expect(p).not.toContain("GENCLASS_DASHBOARD="); + expect(p).not.toContain("GENCLASS_REPORT="); + expect(p).not.toContain("genclass.dev/dashboard"); + expect(p).not.toContain("consoleJson"); + expect(p).not.toContain("fetch-model"); + expect(p).not.toContain("Deploy it"); + // the steps are numbered 1..n with none missing + const nums = [...p.matchAll(/^(\d+)\. /gm)].map((m) => Number(m[1])); + expect(nums).toEqual(nums.map((_, i) => i + 1)); + expect(p).toMatch(/\n8\. Tell me what you changed, that the app sets no GenClass token and has telemetry off, what it downloads from outside this network/); + // only printable text (no step marker or other control character reaches the page) + expect(p).not.toMatch(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/); + }); + + it("the choices: guard and eager, findings in the app's own logs, the model served by the app, deploy", () => { + const p = openSourcePrompt({ guard: true, eager: true, logs: true, selfHost: true, deploy: true }); + expect(p).toContain('import { consoleJson } from "@genclass/runtime/sinks/console-json";'); + expect(p).toContain('const rt = GenClass.init({ telemetry: false, autoState: true, mode: "guard", aggressiveness: "eager", visual: { on: true, fix: true }, model: { baseUrl: "/genclass-model/", ortWasmPaths: "/genclass-model/ort/" }, sinks: [consoleJson()] });'); + expect(p).toContain(''); + expect(p).toContain(``); + expect(p).toContain(`npx ${RUNTIME_PKG} init --no-telemetry --mode guard --sink console-json --model-url /genclass-model/ --yes`); + // init writes no visual or aggressiveness option: the prompt says where they go + expect(p).toContain('add aggressiveness: "eager" and visual: { on: true, fix: true } to the option file it writes (data-visual="fix" on the tag it writes, and the GENCLASS_CONFIG line above; Astro gets a line instead: add visual=fix to its content, and the aggressiveness in a window.GENCLASS_CONFIG script before it); run the fetch-model command below before it.)'); + expect(openSourcePrompt({ guard: true })).toContain('; add visual: { on: true, fix: true } to the option file it writes (data-visual="fix" on the tag it writes; Astro gets a line instead: add visual=fix to its content).)'); + expect(p).toContain("Mode guard: when GenClass is sure"); + expect(p).toContain("the webhook sink sends batched POSTs there instead"); + expect(p).toContain('sinks: [webhook({ url: "" })], requests: { ignore: [""] }'); + expect(p).toContain(`npx ${RUNTIME_PKG} fetch-model public/genclass-model`); + expect(p).toContain('run fetch-model public/genclass-model --variant q8 --ort wasm instead and add device: "wasm" to the model option (data-device="wasm" on the script tag'); + // the model served by the app: no jsDelivr download for it to mention + expect(p).not.toContain("still downloads"); + expect(p).toContain("Deploy it, as chosen"); + expect(p).toMatch(/\n11\. Tell me what you changed and where it is deployed/); + // eager only with guard + expect(openSourcePrompt({ eager: true })).toBe(openSourcePrompt()); + }); + + it("the page: ticking the choices rewrites the copied prompt with the server's own openSourcePrompt, and it sends nothing", async () => { + const r = await openSource(); + expect(r.copy()).toBe(openSourcePrompt()); + expect(r.box("eager").disabled).toBe(true); + r.box("guard").click(); + expect(r.box("eager").disabled).toBe(false); + r.box("eager").click(); + r.box("logs").click(); + r.box("selfHost").click(); + r.box("deploy").click(); + const all = openSourcePrompt({ guard: true, eager: true, logs: true, selfHost: true, deploy: true }); + expect(r.copy()).toBe(all); + expect((r.doc.getElementById("cpt") as unknown as HTMLTextAreaElement).value).toBe(all); + // unticking guard unticks eager + r.box("guard").click(); + expect(r.box("eager").checked).toBe(false); + expect(r.copy()).toBe(openSourcePrompt({ logs: true, selfHost: true, deploy: true })); + (r.doc.getElementById("cp") as unknown as HTMLElement).click(); + await new Promise((x) => setTimeout(x, 5)); + expect(r.copied).toEqual([openSourcePrompt({ logs: true, selfHost: true, deploy: true })]); + // the commands copy too + const cmds = [...r.doc.querySelectorAll(".cmds [data-copy]")].map((b) => b.getAttribute("data-copy")); + expect(cmds).toEqual([ + `npx ${RUNTIME_PKG} init --no-telemetry`, + `// in the app's entry, after the lines init added\nimport { GenClass } from "@genclass/runtime";\nimport { connectAgent } from "@genclass/runtime/agent";\nif (import.meta.env.DEV && GenClass.runtime) connectAgent(GenClass.runtime);`, + `// in the app's entry (Next.js: instrumentation-client.ts), after the lines init added\nimport { GenClass } from "@genclass/runtime";\nimport { connectAgent } from "@genclass/runtime/agent";\nif (process.env.NODE_ENV === "development" && GenClass.runtime) connectAgent(GenClass.runtime);`, + `npx -y ${MCP_PKG} init`, + `npx ${RUNTIME_PKG} scan http://localhost:5173/ --local`, + ]); + // the choices are a fieldset with a visible legend; the commands' titles are h3 under the section's h2 + expect(r.doc.querySelector("fieldset#choices > legend")!.textContent).toContain("What the prompt sets up"); + expect(r.doc.querySelectorAll(".cmds h3").length).toBe(5); + expect(r.doc.querySelectorAll(".cmds h2").length).toBe(1); + expect(r.urls).toEqual([]); + await r.close(); + // no form, no request anywhere in the page, and the way back to the hosted options + expect(r.html).not.toContain(" { + // Chromium restores ticked boxes after the page script has run: the boxes are ticked, the prompt is the default + const r = await openSource(); + r.box("guard").checked = true; + r.box("logs").checked = true; + expect(r.copy()).toBe(openSourcePrompt()); + r.win.dispatchEvent(new r.win.Event("pageshow")); + expect(r.copy()).toBe(openSourcePrompt({ guard: true, logs: true })); + expect(r.box("eager").disabled).toBe(false); + // a restored eager without guard is unticked again + r.box("guard").checked = false; + r.box("eager").checked = true; + r.win.dispatchEvent(new r.win.Event("pageshow")); + expect(r.box("eager").checked).toBe(false); + expect(r.box("eager").disabled).toBe(true); + expect(r.copy()).toBe(openSourcePrompt({ logs: true })); + await r.close(); + // boxes already ticked when the script runs count too + const t = await openSource({ before: (doc) => ((doc.querySelector('#choices [data-choice="selfHost"]') as HTMLInputElement).checked = true) }); + expect(t.copy()).toBe(openSourcePrompt({ selfHost: true })); + await t.close(); + }); + + it("Eager comes back with the page too: it is enabled in the markup and until pageshow, when the browser restores boxes", async () => { + const r = await openSource({ pageshow: false }); + // Chromium skips disabled boxes when it restores them, so Eager must not be disabled before pageshow + expect(r.html).toMatch(//); + expect(r.box("eager").disabled).toBe(false); + expect(r.copy()).toBe(openSourcePrompt()); + r.box("guard").checked = true; + r.box("eager").checked = true; + r.box("logs").checked = true; + r.win.dispatchEvent(new r.win.Event("pageshow")); + expect(r.box("eager").checked).toBe(true); + expect(r.box("eager").disabled).toBe(false); + expect(r.copy()).toBe(openSourcePrompt({ guard: true, eager: true, logs: true })); + await r.close(); + // without guard, pageshow disables eager + const t = await openSource(); + expect(t.box("eager").disabled).toBe(true); + await t.close(); + }); + + it("a copy through the page's own textarea (execCommand) gives the focus back to the button", async () => { + const r = await openSource({ before: (doc) => ((doc as unknown as { execCommand: () => boolean }).execCommand = () => true) }); + const cp = r.doc.getElementById("cp") as unknown as HTMLElement; + cp.focus(); + cp.click(); + expect(cp.textContent).toBe("Copied"); + expect(r.doc.activeElement).toBe(cp); + expect(r.doc.querySelectorAll("textarea").length).toBe(1); + await r.close(); + }); + + it("a clipboard the browser refuses: the prompt shows, focused and selected, to copy by hand", async () => { + const r = await openSource({ refuse: true }); + const cp = r.doc.getElementById("cp") as unknown as HTMLElement, ta = r.doc.getElementById("cpt") as unknown as HTMLTextAreaElement; + expect(ta.hidden).toBe(true); + cp.click(); + await new Promise((x) => setTimeout(x, 5)); + expect(ta.hidden).toBe(false); + expect(r.doc.activeElement).toBe(ta); + expect(ta.value).toBe(openSourcePrompt()); + expect(cp.textContent).toBe("Selected, now copy"); + expect(r.copied).toEqual([]); + await r.close(); + }); + + it("is linked from /start and from the sign-in and sign-up pages", () => { + expect(startPage("n")).toContain('use the open-source parts only'); + for (const html of [loginPage("n", { google: false, github: false, next: null }), signupPage("n", { google: false, github: false, next: "/account/new?name=shop" })]) + expect(html).toContain('set it up without one'); + }); +}); diff --git a/telemetry-worker/test/plans.test.ts b/telemetry-worker/test/plans.test.ts index 0f8475ae..3e0a7b39 100644 --- a/telemetry-worker/test/plans.test.ts +++ b/telemetry-worker/test/plans.test.ts @@ -171,8 +171,8 @@ describe("no trial", () => { expect(html.replace(/Source(\+| )Code(\+| )Pro/g, ""), p).not.toMatch(/\bPro\b/); } expect(pages[0]![1]).toContain("The dashboard is free."); - expect(pages[1]![1]).toContain('

The runtime needs no account. The dashboard is free for every app.

'); - expect(pages[2]![1]).toContain('

The runtime needs no account. The dashboard is free for every app.

'); + expect(pages[1]![1]).toContain('

The runtime needs no account: set it up without one. The dashboard is free for every app.

'); + expect(pages[2]![1]).toContain('

The runtime needs no account: set it up without one. The dashboard is free for every app.

'); }); it("nothing in src/ asks Stripe for a trial or writes the trial's columns", () => { diff --git a/telemetry-worker/wrangler.toml b/telemetry-worker/wrangler.toml index 796dd529..7d0e678a 100644 --- a/telemetry-worker/wrangler.toml +++ b/telemetry-worker/wrangler.toml @@ -30,10 +30,10 @@ routes = [ # The newest @genclass/runtime on npm, on any dist-tag. Below 0.3.0-beta.0 the Connect panel, /start and private # dashboards say first that project keys and `login` are not published yet. Set it (with pages.ts SNIPPET_RUNTIME, then -# deploy) to every release once release.yml has published it and jsDelivr serves it: 0.3.0-beta.3 now, "0.3.0" at 0.3.0 +# deploy) to every release once release.yml has published it and jsDelivr serves it: 0.3.0-beta.4 now, "0.3.0" at 0.3.0 # (with RUNTIME_PKG). [vars] -RUNTIME_ON_NPM = "0.3.0-beta.3" +RUNTIME_ON_NPM = "0.3.0-beta.4" # "on" sells the plans marked managed_payments in billing/plans.json through Stripe Managed Payments (Stripe, through # Link, is the merchant of record, for 3.5% of each payment on top of Stripe's fees). No plan is marked today # (no plan is sold on the site today), so it stays "off"; README "Paid plans".