diff --git a/packages/runtime/README.md b/packages/runtime/README.md index e3929f92..9c5aee17 100644 --- a/packages/runtime/README.md +++ b/packages/runtime/README.md @@ -604,17 +604,20 @@ value, or none, builds without GenClass. Like every `NEXT_PUBLIC_*` value, it is ## For AI coding agents installing GenClass -1. Run `npx @genclass/runtime@beta init --dry-run` (it shows the diff and sends nothing). Tell the user that +1. Run `npx @genclass/runtime@beta init --dry-run --mode heal` (it shows the diff and sends nothing). Tell the user that anonymous diagnostics are on by default on public hosts, and that applying `init` creates the app's dashboard at genclass.dev from `package.json`'s `name` unless they pass `--no-token`. 2. Apply it with `--yes` only once they agree. Telemetry is the user's decision: add `--no-telemetry` or `telemetry: false` only if they ask. -3. Keep the default observe mode. Use `--mode guard` only on request, and then keep payment and sign-in endpoints - observe-only: `requests: { protect: ["preset:payments", "preset:auth"] }` in `window.GENCLASS_CONFIG` (the - `genclass.config.ts` that `init` writes), or `protect: protectPreset("payments", "auth")` with `GenClass.init()`. +3. Use `--mode heal`, the mode genclass.dev's docs and setup prompts recommend (the Modes table above still labels it + experimental): it reports, and fixes what the model is sure of (discards and + rollbacks can be undone; a breaker steps the mode down after repeated undos). Drop the flag only if the user asks + for observe, which only reports. In guard or heal keep payment and sign-in endpoints observe-only: + `requests: { protect: ["preset:payments", "preset:auth"] }` in `window.GENCLASS_CONFIG` (the `genclass.config.ts` that + `init` writes), or `protect: protectPreset("payments", "auth")` with `GenClass.init()`. 4. `.genclass.local` holds the private dashboard link and is gitignored: never paste it into code, docs, commits or pull requests. The token (`gc_...`) is public and belongs in the code. -5. Check: the console shows `[GenClass] Model ready (...). Mode: observe.`; undo with +5. Check: the console shows `[GenClass] Model ready (...). Mode: heal.`; undo with `npx @genclass/runtime@beta remove`. To change GenClass itself, read [AGENTS.md](https://github.com/genclass-dev/GenClass/blob/main/AGENTS.md). diff --git a/telemetry-worker/README.md b/telemetry-worker/README.md index 81a93e3b..693126d7 100644 --- a/telemetry-worker/README.md +++ b/telemetry-worker/README.md @@ -85,9 +85,9 @@ 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`, `possible_issue` and `check_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, checks, ix_views, ix_sessions), `typeSeries` and `checkSeries` (per bucket, detections by diagnosis and measured checks by kind), `recent` (last 50 detections/interventions in the window; possible ones are not listed). `totals` also has `latencyAvgMs`, `modelLoadAvgMs`, `failOpens`, `modelErrors`, `interventions`, `actionLimits`, `detectionsWithPage`, `possibleWithPage` and `checksWithPage`. 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` | +| `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 and no token to paste: the runtime's anonymous diagnostics stay on and `init` makes the app's free dashboard link (it sends the package name, nothing else, and keeps the link in `.genclass.local`); for teams that cannot sign up for a hosted dashboard. Choices (Guard and Heal, both ticked by default: the heal mode, `mode: "heal"`, `data-mode="heal"`, `init --mode heal`, both off observe; Findings in our own logs: the `console-json` sink; Serve the model ourselves: `fetch-model`; Nothing sent to genclass.dev: `telemetry: false`, `data-telemetry="off"` and `init --no-telemetry`, so no token and no dashboard link either, for teams that may send nothing; 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 | +| `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 Heal, both ticked by default: the heal mode, `mode: "heal"`, `data-mode="heal"` and `init --mode heal`; guard alone is guard, both off observe), 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 | Rate limits on batches: `LIMIT`, 1200 per 60 s per client IP (a ceiling for offices and campuses behind one address; since runtime 0.2.2 a page stops sending after 3 failed requests in a row, so a low per-IP limit would silence every diff --git a/telemetry-worker/src/collector.ts b/telemetry-worker/src/collector.ts index fd5e9b22..c760ba65 100644 --- a/telemetry-worker/src/collector.ts +++ b/telemetry-worker/src/collector.ts @@ -11,7 +11,7 @@ // 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/, POST and GET /start, GET /start/, GET /start/open-source (no account, no token), GET +// POST /api/projects, GET /api/projects/, POST and GET /start, GET /start/, GET /start/open-source (no account; no token to paste, init makes one unless the quiet choice is ticked), 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 diff --git a/telemetry-worker/src/pages.ts b/telemetry-worker/src/pages.ts index 19f075ff..0861e84c 100644 --- a/telemetry-worker/src/pages.ts +++ b/telemetry-worker/src/pages.ts @@ -62,7 +62,7 @@ export const LOGO = ''; /** The MCP server for coding agents, pinned as its own install blocks pin it (packages/mcp/src/clients.ts). */ -export const MCP_PKG = "@genclass/mcp@0.2.0"; +export const MCP_PKG = "@genclass/mcp@0.2.2"; /** The package names and URLs installPrompt names; passed in, so the page script runs the same function. */ export interface PromptNames { @@ -74,12 +74,15 @@ export interface PromptNames { } export const PROMPT_NAMES: PromptNames = { pkg: RUNTIME_PKG, cdn: CDN_SCRIPT, agent: `${CDN_SCRIPT}/dist/agent.js`, mcp: MCP_PKG }; -/** What the Connect dialog's choices add to installPrompt; all off is observe, the overlay in development only. */ +/** + * What the Connect dialog's choices add to installPrompt; the dialog ticks DEFAULT_CHOICES, guard and heal both off is + * observe, the overlay in development only. + */ export interface InstallChoices { /** 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; + /** with guard: mode heal, which also retries a failed request or serves its cached answer, hedges a slow call and rolls back a bad write */ + heal?: boolean; /** the devtools overlay on every host, the deployed site too (for demos and staging) */ badge?: boolean; /** the agent link (a private dashboard link): the MCP server's production numbers and the Loop view */ @@ -88,6 +91,13 @@ export interface InstallChoices { deploy?: boolean; } +/** + * What the Connect dialog and /start/open-source tick when they load: guard with heal, the mode genclass.dev + * recommends. The prompt functions add nothing themselves (no choices is observe): the server renders a page's prompt + * with these and the page script reads the boxes, so the prompt always says what the boxes say. + */ +export const DEFAULT_CHOICES = { guard: true, heal: true } as const; + // The palette, type, buttons, header and focus ring of genclass.dev (site/src/css/base.css and components.css), with // the site's values: black ground, bone text, the GenClass mark's green and red-orange, square corners, 1px rules, // Satoshi for text and JetBrains Mono for code. Like the site, every page here is dark only, whatever the visitor's @@ -218,14 +228,14 @@ function snippets(tok, ep){ var cfg = function(o){ return ''] : []), ` `, " 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" : ""}.)`, + ` (Or npx ${n.pkg} init ${flags.join(" ")} 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${mode ? `; 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.") + + (heal + ? "Mode heal, as chosen: when GenClass is sure, it acts with minimal steps that it logs: it drops a stale response, merges or blocks a duplicate submit, fixes a CSS bug on the page, retries a failed request or serves its cached answer, hedges a slow call, and rolls back a bad write or asks your store to resync; discards and rollbacks can be undone, and a breaker steps the mode down after repeated undos." + : 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." + : "Mode observe, as chosen: 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.`], + [`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${quiet ? ": leave GENCLASS_DASHBOARD and GENCLASS_REPORT unset (either one sends to a GenClass dashboard)" : "; in your own settings, not a file in the repository, set GENCLASS_DASHBOARD to the dashboard link from .genclass.local if you want genclass_analytics to read the app's production numbers, and set GENCLASS_REPORT to on to mirror what you find, replay and prove to that dashboard's Loop view; unset, the server sends nothing off this machine"}. 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], @@ -606,7 +635,13 @@ export function openSourcePrompt(c: OpenSourceChoices = {}, n: PromptNames = PRO ...(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.`], + [ + `Tell me what you changed${c.deploy ? " and where it is deployed" : ""}, ` + + (quiet + ? "that the app sets no GenClass token and has telemetry off, what it downloads from outside this network" + : "whether init created the app's dashboard link (then it is in .genclass.local; keep it out of the repository), what the app sends to genclass.dev and 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"); @@ -618,12 +653,13 @@ function fnSource(fn: (...a: never[]) => unknown): string { } /** - * One choice of a prompt's checkboxes (/start/open-source, the Connect dialog); a sub choice is indented. It is not - * disabled in the markup: a browser brings back only enabled boxes with the page, so the page script disables it on - * pageshow, after that restore, while its parent is off (the prompts ignore it then anyway). + * One choice of a prompt's checkboxes (/start/open-source, the Connect dialog); a sub choice is indented, `on` ticks + * it in the markup (DEFAULT_CHOICES). It is never disabled in the markup: a browser brings back only enabled boxes + * with the page, so the page script disables it on pageshow, after that restore, while its parent is off (the prompts + * ignore it then anyway). */ -function choiceOpt(k: string, label: string, note: string, sub = false): string { - return ``; +function choiceOpt(k: string, label: string, note: string, o: { sub?: boolean; on?: boolean } = {}): string { + return ``; } /** The commands' agent link, before its development check (which is the bundler's: init writes the same one). */ @@ -655,11 +691,13 @@ ul.facts code,.cmds p code{font:13px var(--mono);color:var(--ink)} `; /** - * /start/open-source: GenClass's open-source parts with no account or token (openSourcePrompt behind one copy button, - * its choices, what leaves the network, the commands). Its script rewrites the prompt in the browser and sends nothing. + * /start/open-source: GenClass's open-source parts with no account (openSourcePrompt behind one copy button, its + * choices with DEFAULT_CHOICES ticked, what leaves the network, the commands). Its script rewrites the prompt in the + * browser and sends nothing. */ export function openSourcePage(nonce: string): string { - const prompt = openSourcePrompt(); + const d = DEFAULT_CHOICES; + const prompt = openSourcePrompt(d); const cmd = (label: string, code: string) => `

${label}

${esc(code)}
`; const repo = "https://github.com/genclass-dev/GenClass"; @@ -668,15 +706,17 @@ export function openSourcePage(nonce: string): string {

GenClass without an account

The open-source parts only, Apache-2.0: the runtime in your app's pages, and the MCP server and the - page check on your machine. No sign-in, no token, and nothing is sent to genclass.dev.

+ page check on your machine. No sign-in and no token to paste: the runtime's anonymous diagnostics stay on, and the + init command gives the app its free dashboard link; one choice below sends nothing to genclass.dev at all.

- What the prompt sets up (all off: observe, which only reports) - ${choiceOpt("guard", "Guard", "GenClass fixes what it is sure of, reversibly: it drops a stale response, fixes a CSS bug on the page.")} - ${choiceOpt("eager", "Eager", "With Guard: it acts on smaller signals too.", true)} + What the prompt sets up (Guard and Heal are on: the heal mode; both off is observe, which only reports) + ${choiceOpt("guard", "Guard", "GenClass fixes what it is sure of, reversibly: it drops a stale response, fixes a CSS bug on the page.", { on: d.guard })} + ${choiceOpt("heal", "Heal", "With Guard: the heal mode, which also blocks a repeat, retries a failed request or serves its cached answer, hedges a slow call, and rolls back a bad write or asks your store to resync. Wrong interventions: 0.59% on held-out simulated apps at the default setting, 0.00% on held-out real apps (model 0.2.0).", { sub: true, on: d.heal })} ${choiceOpt("logs", "Findings in our own logs", "Each finding also goes to the console as one JSON line, for the log shipper you already run.")} ${choiceOpt("selfHost", "Serve the model ourselves", "The model and onnxruntime-web's wasm come from your app's host, not jsDelivr: for a strict Content-Security-Policy or a closed network. With script tags, the prompt says what else to serve.")} + ${choiceOpt("quiet", "Nothing sent to genclass.dev", "No dashboard link, and the anonymous diagnostics off (telemetry: false): for teams that may send nothing to a hosted service.")} ${choiceOpt("deploy", "Deploy when done", "The agent deploys with the project's own deploy command.")}
@@ -688,7 +728,7 @@ export function openSourcePage(nonce: string): string {

What leaves your network

    -
  • Nothing goes to genclass.dev: there is no token, and the runtime's anonymous diagnostics are off (telemetry: false).
  • +
  • To genclass.dev, with the defaults: the package name, once, when init creates the app's free dashboard link (the link stays in .genclass.local, which init adds to .gitignore), and then the runtime's anonymous diagnostics from public hosts: its decisions and counts with the app's hostname and the route without ids, no text from the page unless the app opts in, never from local hosts (every field: TELEMETRY.md). With Nothing sent to genclass.dev ticked: nothing; there is no token, and telemetry: false turns the diagnostics off.
  • Browsers download public files from jsDelivr (cdn.jsdelivr.net), once, then cached: the model and onnxruntime-web (about 13 MB on WASM, 18 MB on WebGPU) unless you serve them yourself, and on sites without a bundler the runtime's own files unless you serve its dist/ folder. No app data goes with those requests.
  • The MCP server runs on your machine and its bridge listens on 127.0.0.1 only; with GENCLASS_DASHBOARD and GENCLASS_REPORT unset it sends nothing off the machine, and it keeps what it records in .genclass/agent in the project, which git ignores. genclass_scan and scan --local check pages in headless Chromium on your machine. A page URL leaves it only with hosted: true (or scan without --local when Playwright or its Chromium is missing): it then goes to GenClass's hosted page check (genclass-scan.mehar-144.workers.dev), which is not open source.
  • On developer machines, npm install and npx fetch packages from the npm registry (or your mirror), and the page check's Chromium comes from Playwright's download servers; fetch-model (to serve the model yourself) downloads the model and onnxruntime-web from jsDelivr, or from a mirror you name with --from and --ort-from.
  • @@ -697,16 +737,16 @@ export function openSourcePage(nonce: string): string {

Or run the commands yourself

- ${cmd("Add the runtime to your app", `npx ${RUNTIME_PKG} init --no-telemetry`)} + ${cmd("Add the runtime to your app", `npx ${RUNTIME_PKG} init --mode heal`)} ${cmd("Connect the dev pages to the MCP server: Vite", `${AGENT_LINK_HEAD}\nif (import.meta.env.DEV && GenClass.runtime) connectAgent(GenClass.runtime);`)} ${cmd("The same for Next.js and webpack", `${AGENT_LINK_HEAD.replace("the app's entry", "the app's entry (Next.js: instrumentation-client.ts)")}\nif (process.env.NODE_ENV === "development" && GenClass.runtime) connectAgent(GenClass.runtime);`)} ${cmd("Add the MCP server for Claude Code", `npx -y ${MCP_PKG} init`)} ${cmd("Check a page for visual bugs", `npx ${RUNTIME_PKG} scan http://localhost:5173/ --local`)} -

init shows the diff and asks before it writes; add --mode guard for guard. Other agents: npx -y ${MCP_PKG} config <client> prints the block for Cursor, VS Code, Codex, Gemini CLI and others; keep GENCLASS_DASHBOARD and GENCLASS_REPORT out of it. The scan needs Playwright (npm i -D playwright && npx playwright install chromium).

+

init shows the diff and asks before it writes; with --no-telemetry nothing is sent to genclass.dev, and without --mode heal the mode is observe. Other agents: npx -y ${MCP_PKG} config <client> prints the block for Cursor, VS Code, Codex, Gemini CLI and others; with --no-telemetry keep GENCLASS_DASHBOARD and GENCLASS_REPORT out of it; otherwise GENCLASS_DASHBOARD may name the link from .genclass.local, so genclass_analytics reads the app's production numbers. The scan needs Playwright (npm i -D playwright && npx playwright install chromium).

-`; +`; const script = ` (function(){ var openSourcePrompt = ${fnSource(openSourcePrompt)}; @@ -714,17 +754,18 @@ Or create a free account.`; var box = document.getElementById("choices"), cp = document.getElementById("cp"), ta = document.getElementById("cpt"); function on(k){ var i = box.querySelector('[data-choice="' + k + '"]'); return !!(i && i.checked && !i.disabled); } function now(){ - var p = openSourcePrompt({ guard: on("guard"), eager: on("eager"), logs: on("logs"), selfHost: on("selfHost"), deploy: on("deploy") }, NAMES); + var p = openSourcePrompt({ guard: on("guard"), heal: on("heal"), logs: on("logs"), selfHost: on("selfHost"), quiet: on("quiet"), deploy: on("deploy") }, NAMES); cp.setAttribute("data-copy", p); ta.value = p; } - function eagerFollows(){ var g = box.querySelector('[data-choice="guard"]'), e = box.querySelector('[data-choice="eager"]'); e.disabled = !g.checked; if (!g.checked) e.checked = false; } - box.addEventListener("change", function(e){ if (e.target && e.target.getAttribute && e.target.getAttribute("data-choice") === "guard") eagerFollows(); now(); }); + function healFollows(){ var g = box.querySelector('[data-choice="guard"]'), h = box.querySelector('[data-choice="heal"]'); h.disabled = !g.checked; if (!g.checked) h.checked = false; } + box.addEventListener("change", function(e){ if (e.target && e.target.getAttribute && e.target.getAttribute("data-choice") === "guard") healFollows(); now(); }); [].forEach.call(document.querySelectorAll("[data-copy]"), function(b){ b.addEventListener("click", function(){ gcCopy(b, b.getAttribute("data-copy"), b === cp ? ta : b.parentNode.parentNode.querySelector("pre")); }); }); - // a browser can bring ticked boxes back with the page (back, reload); Chromium does it after load, before pageshow and - // without a change event, and skips disabled boxes: so eager is disabled only on pageshow, and the prompt follows then - function sync(){ eagerFollows(); now(); } + // a browser can bring boxes back with the page (back, reload), ticked or unticked; Chromium does it after load, before + // pageshow and without a change event, and skips disabled boxes: so heal (ticked in the markup) is disabled only on + // pageshow, when a restored unticked guard also unticks it, and the prompt follows then + function sync(){ healFollows(); now(); } now(); window.addEventListener("pageshow", sync); })();`; return shell(nonce, "Use GenClass without an account | GenClass", body, script); @@ -2248,12 +2289,12 @@ const DASH_JS = ` function choiceOn(k){ var i = choiceBox && choiceBox.querySelector('[data-choice="' + k + '"]'); return !!(i && i.checked && !i.disabled); } function promptNow(){ var b = document.getElementById("cpinstall"), t = document.getElementById("cpinstallt"); if (!b || !choiceBox) return; - var p = installPrompt(P.token, P.endpoint || null, { guard: choiceOn("guard"), eager: choiceOn("eager"), badge: choiceOn("badge"), deploy: choiceOn("deploy"), agentLink: choiceOn("agent") ? agentLink : null }, NAMES); + var p = installPrompt(P.token, P.endpoint || null, { guard: choiceOn("guard"), heal: choiceOn("heal"), badge: choiceOn("badge"), deploy: choiceOn("deploy"), agentLink: choiceOn("agent") ? agentLink : null }, NAMES); b.setAttribute("data-copy", p); if (t) t.value = p; } if (choiceBox) choiceBox.addEventListener("change", function(e){ var i = e.target, k = i && i.getAttribute ? i.getAttribute("data-choice") : null, err = choiceBox.querySelector(".err"); if (!k) return; - if (k === "guard") { var ea = choiceBox.querySelector('[data-choice="eager"]'); if (ea) { ea.disabled = !i.checked; if (!i.checked) ea.checked = false; } } + if (k === "guard") { var h = choiceBox.querySelector('[data-choice="heal"]'); if (h) { h.disabled = !i.checked; if (!i.checked) h.checked = false; } } if (k === "agent" && i.checked && !agentLink && P.account) { // ticking it makes the link: the first time, or a new one that replaces the app's (the label says which) i.disabled = true; if (err) err.textContent = ""; @@ -2262,9 +2303,10 @@ const DASH_JS = ` } promptNow(); }); - // a browser can bring ticked boxes back with the page (back, reload); Chromium does it after load, before pageshow and - // without a change event, and skips disabled boxes: so eager is disabled only on pageshow, and the prompt follows then - function choicesSync(){ var g0 = choiceBox.querySelector('[data-choice="guard"]'), e0 = choiceBox.querySelector('[data-choice="eager"]'); if (g0 && e0) { e0.disabled = !g0.checked; if (!g0.checked) e0.checked = false; } var a0 = choiceBox.querySelector('[data-choice="agent"]'); if (a0 && a0.checked && !agentLink) a0.checked = false; promptNow(); } + // a browser can bring boxes back with the page (back, reload), ticked or unticked; Chromium does it after load, before + // pageshow and without a change event, and skips disabled boxes: so heal (ticked in the markup) is disabled only on + // pageshow, when a restored unticked guard also unticks it, and the prompt follows then + function choicesSync(){ var g0 = choiceBox.querySelector('[data-choice="guard"]'), h0 = choiceBox.querySelector('[data-choice="heal"]'); if (g0 && h0) { h0.disabled = !g0.checked; if (!g0.checked) h0.checked = false; } var a0 = choiceBox.querySelector('[data-choice="agent"]'); if (a0 && a0.checked && !agentLink) a0.checked = false; promptNow(); } if (choiceBox) { promptNow(); window.addEventListener("pageshow", choicesSync); } function render(S){ @@ -2518,7 +2560,7 @@ export function analyticsPrompt(token: string, where: string, endpoint: string | return [ "Turn on GenClass visitor analytics in this web app: page views, visits, where visitors come from, devices and funnels, on the app's GenClass dashboard.", "", - `1. Install or upgrade the runtime to the newest beta with the project's package manager: npm install ${RUNTIME_PKG} (or pnpm add / yarn add / bun add). 0.2.x has no analytics. If GenClass is not set up in this app yet, run npx ${RUNTIME_PKG} init first.`, + `1. Install or upgrade the runtime to the newest beta with the project's package manager: npm install ${RUNTIME_PKG} (or pnpm add / yarn add / bun add). 0.2.x has no analytics. If GenClass is not set up in this app yet, run npx ${RUNTIME_PKG} init --mode heal first.`, `2. Turn analytics on where GenClass is configured, with this app's token ${token}:`, ` - GenClass.init({ ... }) in code: add analytics: true, for example GenClass.init({ token: "${token}", analytics: true }).`, " - The config object in genclass.config.ts or genclass.config.js (which init writes): add analytics: true to it.", @@ -2541,16 +2583,18 @@ export function analyticsPrompt(token: string, where: string, endpoint: string | */ export function installPrompt(token: string, endpoint: string | null = null, c: InstallChoices = {}, n: PromptNames = PROMPT_NAMES): string { const guard = !!c.guard; - const eager = guard && !!c.eager; + const heal = guard && !!c.heal; + const mode = heal ? "heal" : guard ? "guard" : ""; const badge = !!c.badge; const link = c.agentLink || ""; // GenClass.init has automatic state discovery off unless asked (autoState: true, with the discover import first) const init = [`token: "${token}"`, "autoState: true"]; - if (guard) init.push('mode: "guard"', ...(eager ? ['aggressiveness: "eager"'] : []), "visual: { on: true, fix: true }"); + if (mode) init.push(`mode: "${mode}"`, "visual: { on: true, fix: true }"); if (endpoint) init.push(`telemetry: { endpoint: "${endpoint}" }`); - // the script tag carries the mode, the visual fixes and the overlay; what it cannot carry goes in GENCLASS_CONFIG - const cfg = [...(eager ? ['aggressiveness: "eager"'] : []), ...(endpoint ? [`telemetry: { endpoint: "${endpoint}" }`] : [])]; - const tag = ``; + // the script tag carries the mode, the visual fixes and the overlay; a self-hosted collector's endpoint, which it + // cannot carry, goes in GENCLASS_CONFIG + const cfg = endpoint ? [`telemetry: { endpoint: "${endpoint}" }`] : []; + const tag = ``; const local = '["localhost", "127.0.0.1"].includes(location.hostname)'; const out = [ "Add GenClass to this web app with its project key, so the app's GenClass dashboard shows what breaks for its users in production (double submits, stale responses, failing endpoints, write races), and so you, the coding agent, can see what happens on the dev pages, replay it and prove fixes. Do every step yourself.", @@ -2564,10 +2608,12 @@ export function installPrompt(token: string, endpoint: string | null = null, c: ...(cfg.length ? [` `] : []), ` ${tag}`, " and in the page's scripts the handle (rt below) is GenClass.runtime.", - ` The key is public: it can only add telemetry to this app, so it belongs in front-end code. (With a bundler, npx ${n.pkg} init --token ${token} --yes does steps 1 and 2 and shows the diff.)`, - (guard - ? `3. Mode guard, as chosen on the dashboard: 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' : ""}.` - : "3. Keep the default mode, observe: GenClass reports and never changes what the app does.") + + ` The key is public: it can only add telemetry to this app, so it belongs in front-end code. (With a bundler, npx ${n.pkg} init --token ${token}${mode ? ` --mode ${mode}` : ""} --yes does steps 1 and 2 and shows the diff.)`, + (heal + ? "3. Mode heal, as chosen on the dashboard: when GenClass is sure, it acts with minimal steps that it logs: it drops a stale response, merges or blocks a duplicate submit, fixes a CSS bug on the page, retries a failed request or serves its cached answer, hedges a slow call, and rolls back a bad write or asks your store to resync; discards and rollbacks can be undone, and a breaker steps the mode down after repeated undos." + : guard + ? "3. Mode guard, as chosen on the dashboard: 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." + : "3. Mode observe, as chosen on the dashboard: GenClass reports and never changes what the app does.") + (badge ? ' Mount the devtools overlay on every host, the deployed site too, as chosen on the dashboard, so visitors see what GenClass does: import("@genclass/runtime/devtools").then((d) => d.mountDevtools(rt)); (the script tag\'s data-devtools does the same).' : ' 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).'), @@ -2603,9 +2649,10 @@ interface ConnectOptions { /** The MCP tab (the page script renders it; the locked page has no script for it). Default true. */ mcp?: boolean; /** - * The choices above the prompt (guard, eager, the badge on every host, the agent link, deploy), which the dashboard's - * page script turns into the prompt; none on the locked page. `agent`: the agent link choice, and whether choosing it - * replaces a link the app already has (account mode), or names this private link (private mode). + * The choices above the prompt (guard with heal, both ticked when the page loads, the badge on every host, the agent + * link, deploy), which the dashboard's page script turns into the prompt; none on the locked page, whose prompt + * keeps the ticked defaults. `agent`: the agent link choice, and whether choosing it replaces a link the app already + * has (account mode), or names this private link (private mode). */ choices?: { agent: "new" | "replace" | "this" | null }; } @@ -2623,11 +2670,11 @@ function connectSection(c: ConnectOptions): string { // same-origin URLs, src/cdn/config.ts) const ep = endpoint ? `, telemetry: { endpoint: "${endpoint}" }` : ""; const cfg = (o: string) => ``; - const init = `import { GenClass } from "@genclass/runtime";\n\nGenClass.init({ token: "${token}"${ep} });`; - const tag = `${endpoint ? `${cfg("")}\n` : ""}`; + const init = `import { GenClass } from "@genclass/runtime";\n\nGenClass.init({ token: "${token}", mode: "heal"${ep} });`; + const tag = `${endpoint ? `${cfg("")}\n` : ""}`; const meta = endpoint - ? `\n${cfg(`token: "${token}", `)}\n\n// in your entry file\nimport "@genclass/runtime/auto";` - : `\n\n// in your entry file\nimport "@genclass/runtime/auto";`; + ? `\n${cfg(`token: "${token}", `)}\n\n// in your entry file\nimport "@genclass/runtime/auto/heal";` + : `\n\n// in your entry file\nimport "@genclass/runtime/auto/heal";`; let n = 0; const snip = (label: string, name: string, code: string) => { const id = `csnip${++n}`; @@ -2636,14 +2683,15 @@ function connectSection(c: ConnectOptions): string { const notYet = c.runtimeReady ? "" : `

Not on npm yet. Project keys need @genclass/runtime ${KEY_RUNTIME}, which is not published yet.

`; - const install = installPrompt(token, endpoint); + const d = DEFAULT_CHOICES; + const install = installPrompt(token, endpoint, d); const ch = c.choices; const opt = choiceOpt; const agentNote = !ch?.agent ? "" : ch.agent === "this" ? "uses this private link" : ch.agent === "replace" ? "replaces the agent link" : "makes an agent link"; const choices = ch ? `
- ${opt("guard", "Guard", "fixes what it is sure of")} - ${opt("eager", "Eager", "acts on smaller signals", true)} + ${opt("guard", "Guard", "fixes what it is sure of", { on: d.guard })} + ${opt("heal", "Heal", "With Guard: the heal mode, which also blocks a repeat, retries a failed request or serves its cached answer, hedges a slow call, and rolls back a bad write or asks your store to resync (0.59% wrong interventions on held-out simulated apps, 0.00% on real, model 0.2.0)", { sub: true, on: d.heal })} ${opt("badge", "Badge on the live site", "overlay on every host")} ${ch.agent ? opt("agent", "Connect my agent to production", agentNote) : ""} ${opt("deploy", "Deploy when done", "")} diff --git a/telemetry-worker/test/auth.test.ts b/telemetry-worker/test/auth.test.ts index 33c9a37f..c522f58c 100644 --- a/telemetry-worker/test/auth.test.ts +++ b/telemetry-worker/test/auth.test.ts @@ -670,7 +670,7 @@ describe("accounts: apps owned by an account", () => { const html = await own.text(); expect(html).toContain("Connect to your project"); expect(html).toContain("npx @genclass/runtime@beta login"); // latest on npm is 0.2.x until 0.3.0 - expect(html).toContain(`GenClass.init({ token: "${project.token}" })`); + expect(html).toContain(`GenClass.init({ token: "${project.token}", mode: "heal" })`); expect(html).toContain(`data-token="${project.token}"`); // RUNTIME_ON_NPM unset: the runtime that reads keys is named first expect(html).toContain("Project keys need @genclass/runtime 0.3.0-beta.0, which is not published yet."); @@ -2629,7 +2629,7 @@ describe("POST /start signed in: the one click puts the app in the account", () const j = await handle(new Request(`${ORIGIN}/start`, { method: "POST", headers }), env, { now: () => clock }); expect(j.status).toBe(201); const body = (await j.json()) as Record; - expect(body.command).toBe(`npx @genclass/runtime@beta init --token ${body.token}`); + expect(body.command).toBe(`npx @genclass/runtime@beta init --token ${body.token} --mode heal`); expect(body.copiedUrl).toMatch(/^\/account\/apps\/[^/]+#connect$/); // no trustworthy Origin (CSRF check): nothing is made, the signed-in form asks const before = sql.rows("SELECT COUNT(*) AS n FROM projects")[0]!.n; diff --git a/telemetry-worker/test/bundle.test.ts b/telemetry-worker/test/bundle.test.ts index 3889af08..9c3a1aed 100644 --- a/telemetry-worker/test/bundle.test.ts +++ b/telemetry-worker/test/bundle.test.ts @@ -49,15 +49,21 @@ describe("page scripts survive Wrangler's bundling (keepNames)", () => { expect(scripts.length, name).toBeGreaterThan(0); for (const s of scripts) expect(s, name).not.toContain("__name("); } - // run /start/open-source's script from the bundle: ticking guard puts it in the copied prompt + // run /start/open-source's script from the bundle: guard and heal start ticked, so the copied prompt sets heal; + // ticking logs, then unticking guard, put each change in the copied prompt const html = pages[0]![1]; const script = /'); - expect(g).toContain(``); - expect(g).toContain("3. Mode guard, as chosen on the dashboard"); + const g = installPrompt(t, null, { guard: true, heal: true, badge: true, deploy: true, agentLink: "https://genclass.dev/dashboard/LINK" }); + expect(g).toContain(`const rt = GenClass.init({ token: "${t}", autoState: true, mode: "heal", visual: { on: true, fix: true } });`); + expect(g).toContain(``); + expect(g).toContain(`npx ${RUNTIME_PKG} init --token ${t} --mode heal --yes does steps 1 and 2`); + expect(g).toContain("3. Mode heal, as chosen on the dashboard: when GenClass is sure, it acts with minimal steps that it logs: it drops a stale response, merges or blocks a duplicate submit, fixes a CSS bug on the page, retries a failed request or serves its cached answer, hedges a slow call, and rolls back a bad write or asks your store to resync; discards and rollbacks can be undone, and a breaker steps the mode down after repeated undos."); + // the mode is an option the script tag carries: no GENCLASS_CONFIG line, and no aggressiveness anywhere + expect(g).not.toContain("GENCLASS_CONFIG"); + expect(g).not.toContain("aggressiveness"); expect(g).toContain("Mount the devtools overlay on every host, the deployed site too"); expect(g).toContain(`claude mcp add genclass -e GENCLASS_DASHBOARD=https://genclass.dev/dashboard/LINK -e GENCLASS_REPORT=on -- npx -y ${MCP_PKG}`); expect(g).not.toContain(`npx -y ${MCP_PKG} init`); expect(g).toContain("8. Deploy it, as chosen on the dashboard"); expect(g).toContain("9. Tell me what you changed and where it is deployed"); - // eager only with guard - expect(installPrompt(t, null, { eager: true })).not.toContain("eager"); + // guard alone is mode guard + const gd = installPrompt(t, null, { guard: true }); + expect(gd).toContain(`const rt = GenClass.init({ token: "${t}", autoState: true, mode: "guard", visual: { on: true, fix: true } });`); + expect(gd).toContain(``); + expect(gd).toContain(`npx ${RUNTIME_PKG} init --token ${t} --mode guard --yes does steps 1 and 2`); + expect(gd).toContain("3. Mode guard, as chosen on the dashboard: 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."); + // neither is observe, with no mode flag; heal only with guard + expect(installPrompt(t)).toContain("3. Mode observe, as chosen on the dashboard: GenClass reports and never changes what the app does."); + expect(installPrompt(t)).not.toContain("--mode"); + expect(installPrompt(t, null, { heal: true })).toBe(installPrompt(t)); + // a self-hosted collector's endpoint is the one GENCLASS_CONFIG line left, before the script tag + expect(installPrompt(t, "https://c.example/v1/events", DEFAULT_CHOICES)).toContain( + ` \n `, + ); }); it("Project overview: usage cards from the period's totals, the three problems that need attention first, each with its prompt", async () => { @@ -1078,31 +1093,41 @@ describe("dashboard page: the Supabase Studio frame (src/studio.ts): header, rai 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"); - expect(copied()).toBe(installPrompt(st.token)); - expect(box("eager").disabled).toBe(true); - box("guard").click(); - expect(box("eager").disabled).toBe(false); - box("eager").click(); + // guard and heal start ticked, in the markup too: the prompt sets heal before and after the script runs + expect(copied()).toBe(installPrompt(st.token, null, DEFAULT_CHOICES)); + expect(box("guard").hasAttribute("checked")).toBe(true); + expect(box("heal").hasAttribute("checked")).toBe(true); + expect(box("heal").checked).toBe(true); + expect(box("heal").disabled).toBe(false); + for (const k of ["badge", "agent", "deploy"]) expect(box(k).checked, k).toBe(false); box("badge").click(); box("deploy").click(); - expect(copied()).toBe(installPrompt(st.token, null, { guard: true, eager: true, badge: true, deploy: true })); + expect(copied()).toBe(installPrompt(st.token, null, { guard: true, heal: true, badge: true, deploy: true })); expect((r.doc.getElementById("cpinstallt") as unknown as HTMLTextAreaElement).value).toBe(copied()); // the agent choice makes the agent link (POST .../link), which the prompt then names, as the MCP page does box("agent").click(); await new Promise((x) => setTimeout(x, 10)); const link = `https://genclass.dev/dashboard/${"S".repeat(32)}`; expect(r.writes).toEqual(["POST /api/account/projects/0123456789abcdef/link"]); - expect(copied()).toBe(installPrompt(st.token, null, { guard: true, eager: true, badge: true, deploy: true, agentLink: link })); + expect(copied()).toBe(installPrompt(st.token, null, { guard: true, heal: true, badge: true, deploy: true, agentLink: link })); expect(r.doc.querySelector("#mcp .alink-out code")!.textContent).toBe(link); - // unticking guard unticks eager + // unticking guard unticks and disables heal: observe box("guard").click(); - expect(box("eager").checked).toBe(false); + expect(box("heal").checked).toBe(false); + expect(box("heal").disabled).toBe(true); expect(copied()).toBe(installPrompt(st.token, null, { badge: true, deploy: true, agentLink: link })); + // guard again is guard alone until heal is ticked again + box("guard").click(); + expect(box("heal").disabled).toBe(false); + expect(box("heal").checked).toBe(false); + expect(copied()).toBe(installPrompt(st.token, null, { guard: true, badge: true, deploy: true, agentLink: link })); + box("heal").click(); + expect(copied()).toBe(installPrompt(st.token, null, { guard: true, heal: true, badge: true, deploy: true, agentLink: link })); await r.close(); // a private link names itself; no request const s = await render(st); (s.doc.querySelector('#cchoices [data-choice="agent"]') as unknown as HTMLInputElement).click(); - expect(s.doc.getElementById("cpinstall")!.getAttribute("data-copy")).toBe(installPrompt(st.token, null, { agentLink: "sample" })); + expect(s.doc.getElementById("cpinstall")!.getAttribute("data-copy")).toBe(installPrompt(st.token, null, { ...DEFAULT_CHOICES, agentLink: "sample" })); expect(s.writes).toEqual([]); await s.close(); }); @@ -1112,20 +1137,23 @@ describe("dashboard page: the Supabase Studio frame (src/studio.ts): header, rai 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; + // Chromium restores boxes after the page script has run: a guard the visitor unticked comes back unticked, and heal, + // disabled then and so skipped, keeps the markup's tick; the prompt is still the default until pageshow + box("guard").checked = false; box("badge").checked = true; - expect(copied()).toBe(installPrompt(st.token)); + expect(copied()).toBe(installPrompt(st.token, null, DEFAULT_CHOICES)); 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; + expect(box("heal").checked).toBe(false); + expect(box("heal").disabled).toBe(true); + expect(copied()).toBe(installPrompt(st.token, null, { badge: true })); + // a restored guard without heal, and a restored agent box with no link yet (unticked again; nothing is created) + box("guard").checked = true; box("agent").checked = true; r.win.dispatchEvent(new r.win.Event("pageshow")); - expect(box("eager").checked).toBe(false); + expect(box("heal").disabled).toBe(false); + expect(box("heal").checked).toBe(false); expect(box("agent").checked).toBe(false); - expect(copied()).toBe(installPrompt(st.token, null, { badge: true })); + expect(copied()).toBe(installPrompt(st.token, null, { guard: true, badge: true })); expect(r.writes).toEqual([]); await r.close(); }); @@ -1315,7 +1343,7 @@ describe("dashboard page: measured visual checks (docs/runtime/VISUAL.md section }); }); -describe("/start/open-source: the open-source parts with no account, no token and nothing sent to genclass.dev", () => { +describe("/start/open-source: the open-source parts with no account; heal and the anonymous diagnostics on unless chosen off, and the page sends nothing", () => { /** 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"); @@ -1336,26 +1364,34 @@ describe("/start/open-source: the open-source parts with no account, no token an 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."); + it("the page's default prompt: heal, the anonymous diagnostics on and init's free dashboard link, no token to paste, the agent link, the MCP server with that link as a choice, the local scan", () => { + const p = openSourcePrompt(DEFAULT_CHOICES); + expect(p).toContain( + "No GenClass account and nothing to sign in to: step 2's init command (use it rather than the hand-written GenClass.init(), which has no token and so makes no dashboard link) creates the app's free dashboard link (it sends the package name, nothing else, and saves the private link in .genclass.local, which init adds to .gitignore; keep that link private), and GenClass's anonymous diagnostics stay on: its decisions and counts, no text from the page unless the app opts in, never from local hosts (every field: packages/runtime/TELEMETRY.md in that repository). Do every step yourself.", + ); expect(p).toContain(`npm install ${RUNTIME_PKG}`); - expect(p).toContain("const rt = GenClass.init({ telemetry: false, autoState: true });"); + expect(p).toContain('const rt = GenClass.init({ autoState: true, mode: "heal", visual: { on: true, fix: 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"); + // the diagnostics stay on, and the public files come from jsDelivr unless the app serves them; the prompt says so + expect(p).toContain( + "with GenClass's anonymous diagnostics on (the default: its decisions and counts go to genclass.dev from public hosts, never from local ones); the browser downloads its model and onnxruntime-web, public files, from cdn.jsdelivr.net", + ); + expect(p).toContain(``); + expect(p).toContain(`npx ${RUNTIME_PKG} init --mode heal --yes does steps 1 and 2`); + expect(p).toContain('and adds step 3\'s dev-only overlay; the handle is then GenClass.runtime; 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( + "3. Mode heal, as chosen: when GenClass is sure, it acts with minimal steps that it logs: it drops a stale response, merges or blocks a duplicate submit, fixes a CSS bug on the page, retries a failed request or serves its cached answer, hedges a slow call, and rolls back a bad write or asks your store to resync; discards and rollbacks can be undone, and a breaker steps the mode down after repeated undos. In development builds also mount the devtools overlay", + ); + // nothing turns the diagnostics off, and no aggressiveness or GENCLASS_CONFIG line is left + for (const s of ["telemetry: false", "--no-telemetry", "data-telemetry", "aggressiveness", "GENCLASS_CONFIG", "Keep the default mode"]) expect(p, s).not.toContain(s); 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"); + expect(p).toContain("set GENCLASS_DASHBOARD to the dashboard link from .genclass.local"); + expect(p).not.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`); @@ -1368,7 +1404,7 @@ describe("/start/open-source: the open-source parts with no account, no token an // 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 + // nothing that needs an account, a token to paste 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="); @@ -1379,65 +1415,105 @@ describe("/start/open-source: the open-source parts with no account, no token an // 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/); + expect(p).toMatch( + /\n8\. Tell me what you changed, whether init created the app's dashboard link \(then it is in \.genclass\.local; keep it out of the repository\), what the app sends to genclass\.dev and what it downloads from outside this network, and that a new conversation with you has the GenClass tools\./, + ); // 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 }); + it("the quiet choice: telemetry off in the init line, on the tag and in init's flags, so no token and no dashboard link, nothing sent to genclass.dev", () => { + const p = openSourcePrompt({ ...DEFAULT_CHOICES, quiet: true }); + expect(p).toContain("leave GENCLASS_DASHBOARD and GENCLASS_REPORT unset"); + expect(p).toContain("No GenClass account and no token: nothing is sent to genclass.dev. Do every step yourself."); + expect(p).not.toContain("dashboard link"); + expect(p).toContain('const rt = GenClass.init({ telemetry: false, autoState: true, mode: "heal", visual: { on: true, fix: true } });'); + expect(p).toContain(``); + // --no-telemetry means no token: init creates no project + expect(p).toContain(`npx ${RUNTIME_PKG} init --no-telemetry --mode heal --yes does steps 1 and 2`); + // no data is sent, but the public files still come from jsDelivr unless the app serves them; the prompt says so + expect(p).toContain("with GenClass's anonymous diagnostics off (telemetry: false), 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("3. Mode heal, as chosen: when GenClass is sure"); + 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, and that a new conversation with you has the GenClass tools\./); + expect(p).not.toMatch(/token: "|data-token|--token|gc_[A-Za-z0-9]/); + expect(p).not.toContain("genclass.dev/dashboard"); + // the same steps as the default prompt: only the opening, step 2's first line and the last step change + expect(p.split("\n").length).toBe(openSourcePrompt(DEFAULT_CHOICES).split("\n").length); + // quiet with observe + const o = openSourcePrompt({ quiet: true }); + expect(o).toContain("const rt = GenClass.init({ telemetry: false, autoState: true });"); + expect(o).toContain(``); + expect(o).toContain(`npx ${RUNTIME_PKG} init --no-telemetry --yes does steps 1 and 2 and shows the diff`); + expect(o).toContain("and adds step 3's dev-only overlay; the handle is then GenClass.runtime.)"); + expect(o).toContain("3. Mode observe, as chosen: GenClass reports and never changes what the app does."); + }); + + it("the choices: guard and heal, findings in the app's own logs, the model served by the app, deploy", () => { + const p = openSourcePrompt({ guard: true, heal: 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('const rt = GenClass.init({ autoState: true, mode: "heal", visual: { on: true, fix: true }, model: { baseUrl: "/genclass-model/", ortWasmPaths: "/genclass-model/ort/" }, sinks: [consoleJson()] });'); + expect(p).toContain(``); + expect(p).toContain(`npx ${RUNTIME_PKG} init --mode heal --sink console-json --model-url /genclass-model/ --yes`); + // init writes no visual option: the prompt says where it goes + expect(p).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); run the fetch-model command below before it.)'); + expect(p).toContain("3. Mode heal, as chosen: when GenClass is sure"); + expect(p).not.toContain("GENCLASS_CONFIG"); + expect(p).not.toContain("aggressiveness"); + // guard alone is mode guard + const g = openSourcePrompt({ guard: true }); + expect(g).toContain('const rt = GenClass.init({ autoState: true, mode: "guard", visual: { on: true, fix: true } });'); + expect(g).toContain(``); + expect(g).toContain(`npx ${RUNTIME_PKG} init --mode guard --yes does steps 1 and 2`); + expect(g).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(g).toContain("3. 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. In development builds"); 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).not.toContain("downloads its model"); 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()); + // neither mode choice is observe, with no mode flag; heal only with guard + expect(openSourcePrompt()).toContain("3. Mode observe, as chosen: GenClass reports and never changes what the app does."); + expect(openSourcePrompt()).toContain(`npx ${RUNTIME_PKG} init --yes does steps 1 and 2`); + expect(openSourcePrompt({ heal: 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(); + // guard and heal start ticked: the copied prompt sets heal + expect(r.copy()).toBe(openSourcePrompt(DEFAULT_CHOICES)); + expect(r.box("heal").checked).toBe(true); + expect(r.box("heal").disabled).toBe(false); + for (const k of ["logs", "selfHost", "quiet", "deploy"]) expect(r.box(k).checked, k).toBe(false); r.box("logs").click(); r.box("selfHost").click(); + r.box("quiet").click(); r.box("deploy").click(); - const all = openSourcePrompt({ guard: true, eager: true, logs: true, selfHost: true, deploy: true }); + const all = openSourcePrompt({ guard: true, heal: true, logs: true, selfHost: true, quiet: true, deploy: true }); expect(r.copy()).toBe(all); expect((r.doc.getElementById("cpt") as unknown as HTMLTextAreaElement).value).toBe(all); - // unticking guard unticks eager + // unticking guard unticks and disables heal r.box("guard").click(); - expect(r.box("eager").checked).toBe(false); - expect(r.copy()).toBe(openSourcePrompt({ logs: true, selfHost: true, deploy: true })); + expect(r.box("heal").checked).toBe(false); + expect(r.box("heal").disabled).toBe(true); + expect(r.copy()).toBe(openSourcePrompt({ logs: true, selfHost: true, quiet: 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 })]); + expect(r.copied).toEqual([openSourcePrompt({ logs: true, selfHost: true, quiet: 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`, + `npx ${RUNTIME_PKG} init --mode heal`, `// 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.querySelector("fieldset#choices > legend")!.textContent).toBe("What the prompt sets up (Guard and Heal are on: the heal mode; both off is observe, which only reports)"); expect(r.doc.querySelectorAll(".cmds h3").length).toBe(5); expect(r.doc.querySelectorAll(".cmds h2").length).toBe(1); expect(r.urls).toEqual([]); @@ -1450,45 +1526,64 @@ describe("/start/open-source: the open-source parts with no account, no token an }); it("boxes a browser brings back with the page (back, reload; no change event) rewrite the prompt on pageshow", async () => { - // Chromium restores ticked boxes after the page script has run: the boxes are ticked, the prompt is the default + // Chromium restores boxes after the page script has run: a guard the visitor unticked comes back unticked, and heal, + // disabled then and so skipped, keeps the markup's tick; the prompt is still the default until pageshow 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.box("logs").checked = true; + expect(r.copy()).toBe(openSourcePrompt(DEFAULT_CHOICES)); r.win.dispatchEvent(new r.win.Event("pageshow")); - expect(r.box("eager").checked).toBe(false); - expect(r.box("eager").disabled).toBe(true); + expect(r.box("heal").checked).toBe(false); + expect(r.box("heal").disabled).toBe(true); expect(r.copy()).toBe(openSourcePrompt({ logs: true })); + // a restored guard without heal: heal is enabled again, still unticked + r.box("guard").checked = true; + r.win.dispatchEvent(new r.win.Event("pageshow")); + expect(r.box("heal").disabled).toBe(false); + expect(r.box("heal").checked).toBe(false); + expect(r.copy()).toBe(openSourcePrompt({ guard: true, logs: true })); await r.close(); - // boxes already ticked when the script runs count too + // boxes already changed 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 })); + expect(t.copy()).toBe(openSourcePrompt({ ...DEFAULT_CHOICES, selfHost: true })); await t.close(); + const u = await openSource({ before: (doc) => ((doc.querySelector('#choices [data-choice="guard"]') as HTMLInputElement).checked = false) }); + expect(u.copy()).toBe(openSourcePrompt()); + expect(u.box("heal").checked).toBe(false); + expect(u.box("heal").disabled).toBe(true); + await u.close(); }); - it("Eager comes back with the page too: it is enabled in the markup and until pageshow, when the browser restores boxes", async () => { + it("the defaults are in the markup: guard and heal ticked, nothing else, and heal enabled 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; + // the server renders the prompt the ticked boxes make, so the page says the same without its script + expect(r.html).toContain(`data-copy="${esc(openSourcePrompt(DEFAULT_CHOICES))}"`); + expect(r.html).toMatch(//); + expect(r.html).toMatch(//); + for (const k of ["logs", "selfHost", "quiet", "deploy"]) expect(r.html, k).toMatch(new RegExp(``)); + expect(r.html).not.toContain('data-choice="eager"'); + // Chromium skips disabled boxes when it restores them, so heal must not be disabled before pageshow + expect(r.box("heal").disabled).toBe(false); + expect(r.box("heal").checked).toBe(true); + expect(r.copy()).toBe(openSourcePrompt(DEFAULT_CHOICES)); + // a restored unticked heal under a ticked guard stays unticked, and enabled + r.box("heal").checked = false; 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 })); + expect(r.box("heal").checked).toBe(false); + expect(r.box("heal").disabled).toBe(false); + expect(r.copy()).toBe(openSourcePrompt({ guard: true, logs: true })); await r.close(); - // without guard, pageshow disables eager + // with nothing restored, pageshow keeps the defaults const t = await openSource(); - expect(t.box("eager").disabled).toBe(true); + expect(t.box("heal").checked).toBe(true); + expect(t.box("heal").disabled).toBe(false); + expect(t.copy()).toBe(openSourcePrompt(DEFAULT_CHOICES)); + // the page says what stays on and what the one choice turns off + expect(t.html).toContain("the runtime's anonymous diagnostics stay on"); + expect(t.html).toContain("Nothing sent to genclass.dev"); + expect(t.html).toContain("https://github.com/genclass-dev/GenClass/blob/main/packages/runtime/TELEMETRY.md"); + expect(t.html).toContain("with --no-telemetry nothing is sent to genclass.dev, and without --mode heal the mode is observe"); await t.close(); }); @@ -1511,7 +1606,7 @@ describe("/start/open-source: the open-source parts with no account, no token an await new Promise((x) => setTimeout(x, 5)); expect(ta.hidden).toBe(false); expect(r.doc.activeElement).toBe(ta); - expect(ta.value).toBe(openSourcePrompt()); + expect(ta.value).toBe(openSourcePrompt(DEFAULT_CHOICES)); expect(cp.textContent).toBe("Selected, now copy"); expect(r.copied).toEqual([]); await r.close(); diff --git a/telemetry-worker/test/selfhost.test.ts b/telemetry-worker/test/selfhost.test.ts index 3e88e0d5..3274a3fd 100644 --- a/telemetry-worker/test/selfhost.test.ts +++ b/telemetry-worker/test/selfhost.test.ts @@ -40,17 +40,17 @@ describe("private mode: a self-hosted collector's snippets name its own endpoint const s = snippets("gc_TestToken0123456789abc", EP); expect(s.map((x) => x[0])).toEqual(["Easy: one script tag", "Easy: with a bundler", "Function-specific"]); expect(s[0]![2]).toBe( - `\n`, + `\n`, ); expect(s[1]![2]).toContain(``); - expect(s[1]![2]).toContain('import "@genclass/runtime/auto";'); + expect(s[1]![2]).toContain('import "@genclass/runtime/auto/heal";'); expect(s[1]![2]).not.toContain(' x[0])).toEqual(["Easy: one script tag", "Easy: with a bundler", "Function-specific", "Automatic setup"]); - expect(d[0]![2]).toBe(``); + expect(d[0]![2]).toBe(``); expect(JSON.stringify(d)).not.toContain("GENCLASS_CONFIG"); expect(setupOf(null)).toContain('"endpoint":""'); }); @@ -61,7 +61,7 @@ describe("private mode: a self-hosted collector's snippets name its own endpoint // the Connect panel itself (every page also carries the client-side snippet code, which names GENCLASS_CONFIG) const panel = (h: string) => /
window.GENCLASS_CONFIG = { telemetry: { endpoint: "${EP}" } };`)); expect(own).toContain("Add the key and this collector's endpoint"); expect(own).not.toContain(`npx ${RUNTIME_PKG} login`);