diff --git a/AGENTS.md b/AGENTS.md index 3743d5d..25d363f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,6 +102,12 @@ cargo clippy --locked --all-targets --manifest-path src-tauri/Cargo.toml -- -D w cargo test --locked --lib --manifest-path src-tauri/Cargo.toml ``` +Run `bun run screenshot` to refresh `docs/assets/screenshot.png` from the real +page using its credential-free `?screenshot=1` fixture; the command starts and +stops its own Vite server and browser. The 2240 by 1312 image captures the +1120 by 656 webview content at 2x; the remaining 32px of the 1120 by 688 Tauri +window is native title chrome and is not captured. + ## Commits - [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): diff --git a/bun.lock b/bun.lock index f6a4fa1..28a6bdf 100644 --- a/bun.lock +++ b/bun.lock @@ -19,6 +19,7 @@ "@tailwindcss/vite": "^4.3.3", "@tauri-apps/cli": "^2", "@types/node": "^26.3.0", + "playwright": "1.58.2", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "tailwindcss": "^4.3.3", @@ -268,7 +269,7 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -318,6 +319,10 @@ "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], + + "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], @@ -365,5 +370,9 @@ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], } } diff --git a/docs/assets/screenshot.png b/docs/assets/screenshot.png index f374221..ee94e71 100644 Binary files a/docs/assets/screenshot.png and b/docs/assets/screenshot.png differ diff --git a/package.json b/package.json index 780dd74..f0e17b0 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "screenshot": "bun scripts/capture-screenshot.ts", "tauri": "tauri" }, "license": "MIT", @@ -27,6 +28,7 @@ "@tailwindcss/vite": "^4.3.3", "@tauri-apps/cli": "^2", "@types/node": "^26.3.0", + "playwright": "1.58.2", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "tailwindcss": "^4.3.3", diff --git a/scripts/capture-screenshot.ts b/scripts/capture-screenshot.ts new file mode 100644 index 0000000..0759e6a --- /dev/null +++ b/scripts/capture-screenshot.ts @@ -0,0 +1,93 @@ +import { mkdir } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { chromium } from "playwright"; + +const scriptDirectory = dirname(fileURLToPath(import.meta.url)); +const projectRoot = resolve(scriptDirectory, ".."); +const port = 1430; +const url = `http://127.0.0.1:${port}/?screenshot=1`; +const output = resolve(projectRoot, "docs/assets/screenshot.png"); +const playwrightCli = resolve(projectRoot, "node_modules/playwright/cli.js"); +const viteCli = resolve(projectRoot, "node_modules/vite/bin/vite.js"); + +let server: Bun.Subprocess | undefined; +let browser: Awaited> | undefined; +let stopping: Promise | undefined; + +async function waitForServer() { + const deadline = Date.now() + 30_000; + while (Date.now() < deadline) { + try { + const response = await fetch(url); + if (response.ok) { + return; + } + } catch {} + await Bun.sleep(100); + } + throw new Error(`Vite did not become ready at ${url}`); +} + +async function run(command: string[]) { + const process = Bun.spawn(command, { cwd: projectRoot, stdout: "inherit", stderr: "inherit" }); + if ((await process.exited) !== 0) { + throw new Error(`Command failed: ${command.join(" ")}`); + } +} + +async function stopServer() { + if (!server || server.exitCode !== null) { + return; + } + server.kill("SIGTERM"); + await Promise.race([server.exited.then(() => {}), Bun.sleep(5_000)]); + if (server.exitCode === null) { + server.kill("SIGKILL"); + await server.exited; + } +} + +function cleanup() { + stopping ??= (async () => { + await browser?.close(); + await stopServer(); + })(); + return stopping; +} + +for (const signal of ["SIGINT", "SIGTERM"] as const) { + process.once(signal, () => { + void cleanup().finally(() => process.exit(128)); + }); +} + +try { + await run([process.execPath, playwrightCli, "install", "chromium"]); + server = Bun.spawn( + [process.execPath, viteCli, "dev", "--host", "127.0.0.1", "--port", String(port), "--strictPort"], + { cwd: projectRoot, stdout: "inherit", stderr: "inherit" }, + ); + await waitForServer(); + browser = await chromium.launch(); + const context = await browser.newContext({ + viewport: { width: 1120, height: 656 }, + deviceScaleFactor: 2, + locale: "en-US", + timezoneId: "UTC", + }); + const page = await context.newPage(); + await page.goto(url, { waitUntil: "domcontentloaded" }); + await page.waitForSelector('html[data-screenshot-ready="true"]'); + await page.evaluate(async () => { + await document.fonts.ready; + }); + await page.addStyleTag({ + content: "*, *::before, *::after { animation: none !important; caret-color: transparent !important; transition: none !important; }", + }); + await mkdir(dirname(output), { recursive: true }); + await page.screenshot({ path: output, animations: "disabled" }); + await context.close(); +} finally { + await cleanup(); +} diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..7b8cb97 --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1 @@ +declare const __SCREENSHOT_VERSION__: string; diff --git a/src/lib/screenshot.ts b/src/lib/screenshot.ts new file mode 100644 index 0000000..d7fb095 --- /dev/null +++ b/src/lib/screenshot.ts @@ -0,0 +1,43 @@ +import type { SubnetRow } from "./import"; + +const probedAt = Date.UTC(2026, 7, 24, 14, 30); + +const countries = ["US", "GB", "DE", "CA", "NL", "FR", "JP", "SG", "AU", null]; +const tagSets = [ + ["residential"], + ["mobile"], + ["checkout"], + [], + ["backup"], + ["monitoring"], + ["europe"], + ["retail"], + ["stable"], + [], +]; + +export const screenshotRows: SubnetRow[] = Array.from({ length: 36 }, (_, index) => { + const state = index % 6; + const measured = state !== 5; + const hasMetrics = measured && state !== 4; + const quantity = 24 + ((index * 13) % 77); + const ok = state === 4 ? 0 : measured ? quantity - ((index * 3) % 9) : null; + const base = state === 0 ? 180 : state === 1 ? 720 : state === 2 ? 1840 : 420; + + return { + cidr: `198.18.${index}.0/24`, + country: countries[index % countries.length], + quantity, + tags: tagSets[index % tagSets.length], + ok, + connectP50: hasMetrics ? base : null, + connectP95: hasMetrics ? base + 90 : null, + ttfbP50: hasMetrics ? base + 140 : null, + ttfbP95: hasMetrics ? base + 330 : null, + lastRunAt: measured ? probedAt : null, + }; +}); + +export const screenshotSelectedCidrs = new Set( + screenshotRows.slice(0, 3).map((row) => row.cidr), +); diff --git a/src/lib/ui/MenuSurface.svelte b/src/lib/ui/MenuSurface.svelte index 970e215..0a32d5f 100644 --- a/src/lib/ui/MenuSurface.svelte +++ b/src/lib/ui/MenuSurface.svelte @@ -1,5 +1,5 @@