From b4e5b7cc3c90194dfa87b85c90631ca1fcf1f51d Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Sat, 24 Oct 2026 01:27:06 +0530 Subject: [PATCH 01/44] refactor: restructure extension, share the API contract, redesign the UI Split the extension into background/, lib/ and pages/, move the version, API constants and share links into @meterx/shared, and give the popup, side panel and settings page one design system. The Express server now streams test files and drains uploads instead of buffering them. Also fixes the extension build, which still pointed at the old entry files, and clears dist/ before each build. --- .gitignore | 3 + .junie/guidelines.md | 166 ----- CHANGELOG.md | 4 + packages/extension/build.mjs | 17 +- packages/extension/src/background.ts | 581 ---------------- packages/extension/src/background/index.ts | 119 ++++ .../extension/src/background/measure.test.ts | 85 +++ packages/extension/src/background/measure.ts | 88 +++ packages/extension/src/background/runner.ts | 226 +++++++ packages/extension/src/chart.ts | 72 -- packages/extension/src/format.test.ts | 29 - packages/extension/src/format.ts | 19 - packages/extension/src/history.ts | 49 -- packages/extension/src/isp.ts | 64 -- packages/extension/src/lib/browser.test.ts | 18 + packages/extension/src/lib/browser.ts | 55 ++ .../extension/src/lib/export-data.test.ts | 20 + .../extension/src/{ => lib}/export-data.ts | 21 +- .../extension/src/{ => lib}/export-pdf.ts | 14 +- packages/extension/src/lib/format.test.ts | 46 ++ packages/extension/src/lib/format.ts | 34 + packages/extension/src/lib/history.ts | 28 + packages/extension/src/lib/isp.ts | 26 + .../extension/src/{ => lib}/quality.test.ts | 13 +- packages/extension/src/{ => lib}/quality.ts | 19 +- .../extension/src/lib/report-stats.test.ts | 26 + packages/extension/src/lib/report-stats.ts | 26 + packages/extension/src/lib/tooltips.ts | 44 ++ packages/extension/src/lib/ui.ts | 116 ++++ packages/extension/src/{ => pages}/options.ts | 2 + packages/extension/src/pages/popup.ts | 191 ++++++ packages/extension/src/pages/sidepanel.ts | 196 ++++++ packages/extension/src/popup.ts | 402 ------------ packages/extension/src/share.ts | 61 -- packages/extension/src/sidepanel.ts | 78 --- packages/extension/src/sparkline.ts | 59 -- packages/extension/src/tooltips.ts | 67 -- .../static/fonts/Geist-Variable.woff2 | Bin 0 -> 69492 bytes packages/extension/static/fonts/OFL.txt | 92 +++ packages/extension/static/options.css | 217 ------ packages/extension/static/options.html | 185 +++--- packages/extension/static/popup.css | 556 ---------------- packages/extension/static/popup.html | 191 ++---- packages/extension/static/sidepanel.css | 20 - packages/extension/static/sidepanel.html | 119 ++-- packages/extension/static/styles/app.css | 219 +++++++ packages/extension/static/styles/options.css | 41 ++ packages/extension/static/styles/panel.css | 61 ++ packages/extension/static/styles/popup.css | 40 ++ packages/extension/static/vendor/charts.js | 618 ++++++++++++++++++ packages/extension/tsconfig.json | 11 +- packages/server/package.json | 2 +- packages/server/src/app.test.ts | 64 +- packages/server/src/app.ts | 112 ++-- packages/server/src/test.ts | 89 --- packages/server/tsconfig.json | 14 +- packages/shared/package.json | 5 +- packages/shared/src/index.ts | 109 +-- .../{extension => shared}/src/share.test.ts | 16 +- packages/shared/src/share.ts | 65 ++ packages/shared/tsconfig.json | 7 +- packages/worker/package.json | 7 +- packages/worker/src/index.test.ts | 50 ++ packages/worker/src/index.ts | 250 +++---- packages/worker/tsconfig.json | 15 +- packages/worker/wrangler.toml | 14 +- 66 files changed, 3110 insertions(+), 3163 deletions(-) delete mode 100644 .junie/guidelines.md delete mode 100644 packages/extension/src/background.ts create mode 100644 packages/extension/src/background/index.ts create mode 100644 packages/extension/src/background/measure.test.ts create mode 100644 packages/extension/src/background/measure.ts create mode 100644 packages/extension/src/background/runner.ts delete mode 100644 packages/extension/src/chart.ts delete mode 100644 packages/extension/src/format.test.ts delete mode 100644 packages/extension/src/format.ts delete mode 100644 packages/extension/src/history.ts delete mode 100644 packages/extension/src/isp.ts create mode 100644 packages/extension/src/lib/browser.test.ts create mode 100644 packages/extension/src/lib/browser.ts create mode 100644 packages/extension/src/lib/export-data.test.ts rename packages/extension/src/{ => lib}/export-data.ts (79%) rename packages/extension/src/{ => lib}/export-pdf.ts (95%) create mode 100644 packages/extension/src/lib/format.test.ts create mode 100644 packages/extension/src/lib/format.ts create mode 100644 packages/extension/src/lib/history.ts create mode 100644 packages/extension/src/lib/isp.ts rename packages/extension/src/{ => lib}/quality.test.ts (74%) rename packages/extension/src/{ => lib}/quality.ts (62%) create mode 100644 packages/extension/src/lib/report-stats.test.ts create mode 100644 packages/extension/src/lib/report-stats.ts create mode 100644 packages/extension/src/lib/tooltips.ts create mode 100644 packages/extension/src/lib/ui.ts rename packages/extension/src/{ => pages}/options.ts (99%) create mode 100644 packages/extension/src/pages/popup.ts create mode 100644 packages/extension/src/pages/sidepanel.ts delete mode 100644 packages/extension/src/popup.ts delete mode 100644 packages/extension/src/share.ts delete mode 100644 packages/extension/src/sidepanel.ts delete mode 100644 packages/extension/src/sparkline.ts delete mode 100644 packages/extension/src/tooltips.ts create mode 100644 packages/extension/static/fonts/Geist-Variable.woff2 create mode 100644 packages/extension/static/fonts/OFL.txt delete mode 100644 packages/extension/static/options.css delete mode 100644 packages/extension/static/popup.css delete mode 100644 packages/extension/static/sidepanel.css create mode 100644 packages/extension/static/styles/app.css create mode 100644 packages/extension/static/styles/options.css create mode 100644 packages/extension/static/styles/panel.css create mode 100644 packages/extension/static/styles/popup.css create mode 100644 packages/extension/static/vendor/charts.js delete mode 100644 packages/server/src/test.ts rename packages/{extension => shared}/src/share.test.ts (56%) create mode 100644 packages/shared/src/share.ts create mode 100644 packages/worker/src/index.test.ts diff --git a/.gitignore b/.gitignore index befd6e3..cb50820 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ Thumbs.db # graphify knowledge graph (local) graphify-out/ + +# taste-skill working files (local) +.taste-skill/ diff --git a/.junie/guidelines.md b/.junie/guidelines.md deleted file mode 100644 index ee6a4ca..0000000 --- a/.junie/guidelines.md +++ /dev/null @@ -1,166 +0,0 @@ -# MeterX Development Guidelines - -This document provides essential information for developers working on the MeterX project. It includes build/configuration instructions, testing information, and additional development details. - -## Build and Configuration Instructions - -### Server Setup - -1. **Prerequisites**: - - Node.js (LTS version recommended) - - npm or yarn - -2. **Installation**: - ```powershell - # Navigate to the server directory - cd server - - # Install dependencies - npm install - ``` - -3. **Configuration**: - - The server uses port 3000 by default - - You can modify the port by changing the `PORT` environment variable or updating the `PORT` constant in `server.js` - - Test files are stored in the `test-files` directory - - The server automatically generates test files of different sizes (1MB, 10MB, 25MB) if they don't exist - -4. **Running the Server**: - ```powershell - # Start the server - npm start - - # Or directly with Node.js - node server.js - ``` - -### Extension Setup - -1. **Prerequisites**: - - Chrome, Edge, or Firefox browser - -2. **Loading the Extension**: - - Chrome/Edge: - ``` - 1. Open chrome://extensions or edge://extensions - 2. Enable "Developer mode" - 3. Click "Load unpacked" - 4. Select the "extension" directory - ``` - - Firefox: - ``` - 1. Open about:debugging#/runtime/this-firefox - 2. Click "Load Temporary Add-on" - 3. Select the manifest.json file in the "extension" directory - ``` - -3. **Configuration**: - - The extension is configured to use the live server at `https://meterx-speedtest-server.onrender.com` - - To use a local server, modify `TEST_SERVER_BASE_URL` in `extension/background.js`: - ```javascript - // Change this: - const TEST_SERVER_BASE_URL = 'https://meterx-speedtest-server.onrender.com'; - // To this: - const TEST_SERVER_BASE_URL = 'http://localhost:3000'; - ``` - -## Testing Information - -### Server Testing - -1. **Running Tests**: - ```powershell - # Navigate to the server directory - cd server - - # Start the server in one terminal - npm start - - # Run tests in another terminal - npm test - ``` - -2. **Test Coverage**: - - The test script (`test.js`) verifies the following endpoints: - - `/ping`: Checks server responsiveness - - `/test-file/1MB.bin`: Verifies download functionality - - `/upload`: Confirms upload endpoint works - -3. **Adding New Tests**: - - Add new test cases to the `runTests()` function in `test.js` - - Follow the existing pattern: - ```javascript - // Test example: New endpoint - console.log('\nTest 4: Testing new endpoint...'); - const response = await makeRequest(`${SERVER_URL}/new-endpoint`); - assert.strictEqual(response.statusCode, 200, 'New endpoint should return 200 status code'); - console.log('✅ New endpoint test passed!'); - ``` - -4. **Testing Different File Sizes**: - - The server supports multiple file sizes (1MB, 10MB, 25MB) - - To test different file sizes, modify the URL in the test: - ```javascript - // Change this: - const downloadResponse = await makeRequest(`${SERVER_URL}/test-file/1MB.bin`); - // To this: - const downloadResponse = await makeRequest(`${SERVER_URL}/test-file/10MB.bin`); - ``` - -### Extension Testing - -The extension doesn't have automated tests. Manual testing can be performed as follows: - -1. Load the extension in your browser -2. Click the extension icon to open the popup -3. Click "Start Test" to run the speed test -4. Verify the results display correctly - -## Code Style and Development Guidelines - -1. **JavaScript Style**: - - The project uses ESLint for code quality - - ECMAScript 2021 features are supported - - Run linting with: - ```powershell - # From the project root - npm run lint:fix - ``` - -2. **Project Structure**: - - `server/`: Contains the Node.js/Express server - - `server.js`: Main server implementation - - `test.js`: Server tests - - `test-files/`: Binary files for download tests - - `uploads/`: Directory for upload test files - - `extension/`: Contains the browser extension - - `background.js`: Background script for the extension - - `popup.html`, `popup.css`, `popup.js`: Extension UI - - `manifest.json`: Extension configuration - - `.junie/`: Development guidelines and documentation - -3. **Debugging Tips**: - - Server logs are output to the console - - For extension debugging: - - Chrome/Edge: Right-click the extension icon → "Inspect popup" - - Firefox: Navigate to about:debugging → "Inspect" on the extension - -4. **Performance Considerations**: - - The download test uses a 1MB file by default - - For more accurate results with faster connections, consider using larger test files (10MB or 25MB) - - The server has rate limiting to prevent abuse - -## Deployment - -1. **Server Deployment**: - - The server can be deployed to any Node.js hosting service - - Current production server: `https://meterx-speedtest-server.onrender.com` - - Required environment variables: None (but `PORT` can be set if needed) - -2. **Extension Publishing**: - - Chrome Web Store: - 1. Create a ZIP file of the `extension` directory - 2. Upload to the Chrome Developer Dashboard - - Firefox Add-ons: - 1. Create a ZIP file of the `extension` directory - 2. Upload to the Firefox Add-on Developer Hub \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 969cb74..46fbed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to MeterX are documented here. The format follows [Keep a Ch ## [Unreleased] ### Changed +- Extension source split into `background/`, `lib/` and `pages/`; API constants, version and share links now live in `@meterx/shared` +- New look for the popup, side panel and settings page: one design system (`styles/app.css`), Geist font, light and dark themes +- Self-host server streams test files from memory instead of writing them to disk, and drains uploads without buffering them +- Extension build clears `dist/` first, so deleted files no longer ship in the zip - CHANGELOG moved to Keep a Changelog format and past releases tagged as `v2026.0.0`, `v2026.1.0` and `v2026.2.0` (#91) ## [2026.2.0] - 2026-04-23 diff --git a/packages/extension/build.mjs b/packages/extension/build.mjs index c07c950..32037f3 100644 --- a/packages/extension/build.mjs +++ b/packages/extension/build.mjs @@ -1,18 +1,21 @@ import { build } from 'esbuild'; -import { cpSync, mkdirSync } from 'fs'; +import { cpSync, mkdirSync, rmSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); +// Start clean so files deleted from static/ don't linger in dist/ +rmSync(resolve(__dirname, 'dist'), { recursive: true, force: true }); + // Bundle TypeScript source files await build({ - entryPoints: [ - resolve(__dirname, 'src/background.ts'), - resolve(__dirname, 'src/popup.ts'), - resolve(__dirname, 'src/options.ts'), - resolve(__dirname, 'src/sidepanel.ts'), - ], + entryPoints: { + background: resolve(__dirname, 'src/background/index.ts'), + popup: resolve(__dirname, 'src/pages/popup.ts'), + options: resolve(__dirname, 'src/pages/options.ts'), + sidepanel: resolve(__dirname, 'src/pages/sidepanel.ts'), + }, bundle: true, outdir: resolve(__dirname, 'dist'), target: 'chrome110', diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts deleted file mode 100644 index 7cdb89e..0000000 --- a/packages/extension/src/background.ts +++ /dev/null @@ -1,581 +0,0 @@ -/** - * MeterX Background Service Worker - * @author ajithakdev (https://github.com/ajithakdev) - * @license AGPL-3.0 - * @version 2026.0.319-MR1.0 - */ - -/// - -import type { TestProgress, TestResults, PingResult } from '@meterx/shared'; - -// --- Configuration --- -const DEFAULT_SERVER_URL = 'https://meterx-speedtest.meterx-ajithakdev.workers.dev'; -const DOWNLOAD_CHUNK_MB = 25; // biggest allowed chunk per stream (server whitelist max) -const DOWNLOAD_CHUNK_PATH = `/test-file/${DOWNLOAD_CHUNK_MB}MB.bin`; -const UPLOAD_CHUNK_MB = 5; // body size per POST stream -const UPLOAD_ENDPOINT_PATH = '/upload'; -const PING_ENDPOINT_PATH = '/ping'; -const PING_COUNT = 5; -// Multi-stream duration-based measurement — matches M-Lab / Ookla approach. -// 4 parallel streams let us saturate typical home links up to ~gigabit. -const PARALLEL_DOWNLOAD_STREAMS = 4; -const PARALLEL_UPLOAD_STREAMS = 3; -const MEASURE_DURATION_MS = 8000; -const RAMP_UP_TRIM_MS = 1200; // drop first 1.2s while TCP ramps up -const TAIL_TRIM_MS = 500; // drop last 0.5s to avoid partial chunk skew -const FETCH_PING_TIMEOUT = 5000; -const LOADED_PING_INTERVAL_MS = 300; // probe ping every 300ms during load -const SCHEDULE_ALARM_NAME = 'meterx-scheduled-test'; -const QUALITY_DROP_NOTIFICATION_ID = 'meterx-quality-drop'; - -// --- Server URL (configurable via options page) --- -async function getServerUrl(): Promise { - try { - const result = await chrome.storage.sync.get('serverUrl'); - return result.serverUrl || DEFAULT_SERVER_URL; - } catch { - return DEFAULT_SERVER_URL; - } -} - -// --- Bufferbloat grading --- -function gradeBufferbloat(deltaMs: number): 'A' | 'B' | 'C' | 'D' | 'F' { - if (deltaMs <= 5) return 'A'; - if (deltaMs <= 30) return 'B'; - if (deltaMs <= 60) return 'C'; - if (deltaMs <= 200) return 'D'; - return 'F'; -} - -// --- Messaging Helpers --- -function cleanData(data: Record): Record { - return Object.fromEntries(Object.entries(data).filter(([, v]) => v !== undefined)); -} - -async function sendProgress(data: TestProgress): Promise { - try { - await chrome.runtime.sendMessage({ action: "testProgress", data: cleanData(data as unknown as Record) }); - } catch { /* Popup might be closed */ } -} - -async function sendCompletion(data: TestResults): Promise { - try { - await chrome.runtime.sendMessage({ action: "testComplete", data: cleanData(data as unknown as Record) }); - } catch { /* Popup might be closed */ } -} - -async function sendError(errorMessage: string): Promise { - try { - await chrome.runtime.sendMessage({ action: "testError", error: errorMessage }); - } catch { /* Popup might be closed */ } -} - -// --- Fetch with Timeout --- -async function fetchWithTimeout(resource: string, options: RequestInit = {}, timeout: number): Promise { - const controller = new AbortController(); - const id = setTimeout(() => controller.abort(), timeout); - try { - const response = await fetch(resource, { - ...options, - signal: controller.signal - }); - clearTimeout(id); - return response; - } catch (error: unknown) { - clearTimeout(id); - if (error instanceof Error && error.name === 'AbortError') { - throw new Error(`Request timed out after ${timeout / 1000}s`); - } - throw error; - } -} - -// --- Test Functions --- - -// Loaded-ping probe: fires HEAD /ping on an interval while a flag is live. -// Returns array of observed latencies in ms. Never throws — individual probe -// failures are silently skipped because we only care about aggregate delay. -interface LoadedPingHandle { - stop: () => Promise; -} - -function startLoadedPingProbe(baseUrl: string): LoadedPingHandle { - const samples: number[] = []; - let stopped = false; - let wakeSleep: (() => void) | null = null; - - async function loop(): Promise { - while (!stopped) { - const t0 = performance.now(); - try { - const ctl = new AbortController(); - const to = setTimeout(() => ctl.abort(), FETCH_PING_TIMEOUT); - const res = await fetch(`${baseUrl}${PING_ENDPOINT_PATH}?load=${Date.now()}`, { method: 'HEAD', cache: 'no-store', signal: ctl.signal }); - clearTimeout(to); - if (res.ok) samples.push(performance.now() - t0); - } catch { /* skip */ } - if (stopped) break; - // Sleep, but be interruptible by stop() so the loop doesn't hang - // the outer `await runner` when tests cancel fast. - await new Promise(resolve => { - const timer = setTimeout(() => { wakeSleep = null; resolve(); }, LOADED_PING_INTERVAL_MS); - wakeSleep = () => { clearTimeout(timer); wakeSleep = null; resolve(); }; - }); - } - } - - const runner = loop(); - - return { - stop: async () => { - stopped = true; - if (wakeSleep) wakeSleep(); - await runner; - return samples; - }, - }; -} - -async function detectIpFamily(baseUrl: string): Promise<{ ipv4Ok?: boolean; ipv6Ok?: boolean }> { - try { - const res = await fetchWithTimeout(`${baseUrl}/ip?t=${Date.now()}`, { cache: 'no-store' }, 5000); - if (!res.ok) return {}; - const data = await res.json() as { family?: string }; - if (data.family === 'ipv4') return { ipv4Ok: true }; - if (data.family === 'ipv6') return { ipv6Ok: true }; - return {}; - } catch { - return {}; - } -} - -function extractDnsMs(urlPrefix: string): number | undefined { - // Scan recent resource entries for the most recent request to our server. - // Returns domainLookupEnd - domainLookupStart if the origin honored - // Timing-Allow-Origin; otherwise returns undefined (zeros are filtered out). - try { - const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[]; - for (let i = entries.length - 1; i >= 0; i--) { - const e = entries[i]; - if (!e.name.startsWith(urlPrefix)) continue; - const dns = e.domainLookupEnd - e.domainLookupStart; - if (dns > 0) return dns; - } - } catch { /* no perf api */ } - return undefined; -} - -// Timestamped cumulative-bytes sample used by windowed speed calc. -interface ByteSample { t: number; bytes: number } - -// Given samples ordered by t, return cumulative bytes at time tTarget -// by linear interpolation between adjacent samples. -function bytesAtTime(samples: ByteSample[], tTarget: number): number { - if (samples.length === 0) return 0; - if (tTarget <= samples[0].t) return samples[0].bytes; - if (tTarget >= samples[samples.length - 1].t) return samples[samples.length - 1].bytes; - for (let i = 1; i < samples.length; i++) { - if (samples[i].t >= tTarget) { - const a = samples[i - 1]; - const b = samples[i]; - const ratio = (tTarget - a.t) / (b.t - a.t || 1); - return a.bytes + (b.bytes - a.bytes) * ratio; - } - } - return samples[samples.length - 1].bytes; -} - -function speedFromWindow(samples: ByteSample[], startMs: number, endMs: number): number { - const durS = (endMs - startMs) / 1000; - if (durS <= 0) return 0; - const delta = bytesAtTime(samples, endMs) - bytesAtTime(samples, startMs); - return (delta * 8 / durS) / (1024 * 1024); -} - -async function measureDownloadSpeed(baseUrl: string): Promise { - // Warm-up: establish TCP + TLS connection before timing - await sendProgress({ status: 'Warming up connection...' }); - try { - await fetchWithTimeout(`${baseUrl}/ping?warmup=${Date.now()}`, { method: 'HEAD', cache: 'no-store' }, 5000); - } catch { /* warmup failure is non-fatal */ } - - await sendProgress({ status: 'Downloading test file...' }); - - let totalBytes = 0; - const startTime = performance.now(); - const testEndTarget = startTime + MEASURE_DURATION_MS; - const samples: ByteSample[] = [{ t: 0, bytes: 0 }]; - const controllers: AbortController[] = []; - let lastProgressMs = 0; - - async function streamOne(streamId: number): Promise { - while (performance.now() < testEndTarget && !testAbortController?.signal.aborted) { - const ctl = new AbortController(); - controllers.push(ctl); - try { - const res = await fetch(`${baseUrl}${DOWNLOAD_CHUNK_PATH}?t=${Date.now()}_${streamId}`, { - cache: 'no-store', - signal: ctl.signal, - }); - if (!res.ok || !res.body) throw new Error(`stream ${streamId} http ${res.status}`); - const reader = res.body.getReader(); - while (true) { - const { done, value } = await reader.read(); - if (done) break; - if (performance.now() >= testEndTarget) { - try { await reader.cancel(); } catch { /* ignore */ } - try { ctl.abort(); } catch { /* ignore */ } - break; - } - totalBytes += value.byteLength; - const t = performance.now() - startTime; - samples.push({ t, bytes: totalBytes }); - - if (t - lastProgressMs > 250) { - lastProgressMs = t; - const elapsedS = t / 1000; - if (elapsedS > 0.5) { - const liveSpeed = (totalBytes * 8 / elapsedS) / (1024 * 1024); - await sendProgress({ downloadSpeed: liveSpeed, status: 'Downloading...' }); - } - } - } - } catch { /* stream dropped or aborted — another iteration will take over */ } - } - } - - const streams = Array.from({ length: PARALLEL_DOWNLOAD_STREAMS }, (_, i) => streamOne(i)); - const hardDeadline = new Promise(resolve => setTimeout(resolve, MEASURE_DURATION_MS + 2000)); - await Promise.race([Promise.all(streams), hardDeadline]); - controllers.forEach(c => { try { c.abort(); } catch { /* ignore */ } }); - - // Windowed speed: trim ramp-up and tail to get steady-state throughput. - const usableEnd = Math.min(MEASURE_DURATION_MS - TAIL_TRIM_MS, performance.now() - startTime); - const windowStart = RAMP_UP_TRIM_MS; - if (usableEnd <= windowStart) { - // test was too short — fall back to naive total/time - const totalS = (performance.now() - startTime) / 1000 || 0.001; - const speed = (totalBytes * 8 / totalS) / (1024 * 1024); - await sendProgress({ downloadSpeed: speed, status: 'Download complete' }); - return speed; - } - const speedMbps = speedFromWindow(samples, windowStart, usableEnd); - await sendProgress({ downloadSpeed: speedMbps, status: 'Download complete' }); - return speedMbps; -} - -async function measureUploadSpeed(baseUrl: string): Promise { - await sendProgress({ status: 'Preparing upload...' }); - // Warm-up for upload path - try { - await fetchWithTimeout(`${baseUrl}/ping?warmup_ul=${Date.now()}`, { method: 'HEAD', cache: 'no-store' }, 5000); - } catch { /* non-fatal */ } - - await sendProgress({ status: 'Uploading test data...' }); - - const uploadSize = UPLOAD_CHUNK_MB * 1024 * 1024; - const payload = new Uint8Array(uploadSize); // shared across streams, immutable - - let totalBytes = 0; - const startTime = performance.now(); - const endTarget = startTime + MEASURE_DURATION_MS; - const samples: ByteSample[] = [{ t: 0, bytes: 0 }]; - const controllers: AbortController[] = []; - let lastProgressMs = 0; - - async function uploadOne(streamId: number): Promise { - while (performance.now() < endTarget && !testAbortController?.signal.aborted) { - const ctl = new AbortController(); - controllers.push(ctl); - const chunkStart = performance.now(); - try { - const res = await fetch(`${baseUrl}${UPLOAD_ENDPOINT_PATH}?t=${Date.now()}_${streamId}`, { - method: 'POST', - cache: 'no-store', - headers: { 'Content-Type': 'application/octet-stream' }, - body: payload, - signal: ctl.signal, - }); - if (!res.ok) throw new Error(`upload ${streamId} http ${res.status}`); - await res.text(); - // Credit bytes at completion time (fetch API won't stream upload progress in MV3) - totalBytes += uploadSize; - const t = performance.now() - startTime; - samples.push({ t, bytes: totalBytes }); - - if (t - lastProgressMs > 250) { - lastProgressMs = t; - const elapsedS = t / 1000; - if (elapsedS > 0.5) { - const liveSpeed = (totalBytes * 8 / elapsedS) / (1024 * 1024); - await sendProgress({ uploadSpeed: liveSpeed, status: 'Uploading...' }); - } - } - // Avoid an endless-fast loop for tiny chunks - if (performance.now() - chunkStart < 50) { - await new Promise(r => setTimeout(r, 50)); - } - } catch { /* drop this stream iteration */ } - } - } - - const streams = Array.from({ length: PARALLEL_UPLOAD_STREAMS }, (_, i) => uploadOne(i)); - const hardDeadline = new Promise(resolve => setTimeout(resolve, MEASURE_DURATION_MS + 2000)); - await Promise.race([Promise.all(streams), hardDeadline]); - controllers.forEach(c => { try { c.abort(); } catch { /* ignore */ } }); - - const usableEnd = Math.min(MEASURE_DURATION_MS - TAIL_TRIM_MS, performance.now() - startTime); - const windowStart = RAMP_UP_TRIM_MS; - if (usableEnd <= windowStart) { - const totalS = (performance.now() - startTime) / 1000 || 0.001; - const speed = (totalBytes * 8 / totalS) / (1024 * 1024); - await sendProgress({ uploadSpeed: speed, status: 'Upload complete' }); - return speed; - } - const speedMbps = speedFromWindow(samples, windowStart, usableEnd); - await sendProgress({ uploadSpeed: speedMbps, status: 'Upload complete' }); - return speedMbps; -} - -async function measurePingAndJitter(baseUrl: string): Promise { - await sendProgress({ status: 'Measuring latency...' }); - const url = `${baseUrl}${PING_ENDPOINT_PATH}?t=`; - const latencies: number[] = []; - let lost = 0; - - for (let i = 0; i < PING_COUNT; i++) { - const startTime = performance.now(); - try { - const response = await fetchWithTimeout( - `${url}${Date.now()}_${i}`, - { method: 'HEAD', cache: 'no-store' }, - FETCH_PING_TIMEOUT - ); - if (!response.ok) throw new Error(`Ping failed: ${response.status}`); - const endTime = performance.now(); - const latency = endTime - startTime; - latencies.push(latency); - await sendProgress({ status: `Pong ${i + 1}/${PING_COUNT}! (${latency.toFixed(1)}ms)` }); - } catch (error: unknown) { - const msg = error instanceof Error ? error.message : 'Unknown error'; - console.error(`Ping ${i + 1} error:`, msg); - lost++; - await sendProgress({ status: `Ping ${i + 1}/${PING_COUNT} lost` }); - } - if (i < PING_COUNT - 1) await new Promise(resolve => setTimeout(resolve, 200)); - } - - if (latencies.length === 0) { - throw new Error(`All ${PING_COUNT} pings failed (100% packet loss)`); - } - - const averagePing = latencies.reduce((sum, l) => sum + l, 0) / latencies.length; - let jitterSum = 0; - for (let i = 0; i < latencies.length - 1; i++) { - jitterSum += Math.abs(latencies[i + 1] - latencies[i]); - } - const jitter = latencies.length > 1 ? jitterSum / (latencies.length - 1) : 0; - const packetLoss = (lost / PING_COUNT) * 100; - - await sendProgress({ - ping: averagePing, - jitter, - packetLoss, - status: `Latency measured${lost > 0 ? ` (${packetLoss.toFixed(0)}% loss)` : ''}` - }); - return { ping: averagePing, jitter, packetLoss }; -} - -// --- Main Test Orchestration --- -let isTestRunning = false; -let testAbortController: AbortController | null = null; - -function checkCancelled(): void { - if (testAbortController?.signal.aborted) { - throw new Error('Test cancelled'); - } -} - -async function runFullTest(): Promise { - if (isTestRunning) { - return { status: "Test in progress..." }; - } - isTestRunning = true; - testAbortController = new AbortController(); - - const results: TestResults = { - downloadSpeed: undefined, - uploadSpeed: undefined, - ping: undefined, - jitter: undefined, - packetLoss: undefined, - status: 'Initializing test...' - }; - - try { - // Resolve server URL once at the start - const baseUrl = await getServerUrl(); - await sendProgress({ status: results.status }); - - // IP family detect runs early and non-fatally - detectIpFamily(baseUrl).then(f => { - results.ipv4Ok = f.ipv4Ok; - results.ipv6Ok = f.ipv6Ok; - }).catch(() => { /* non-fatal */ }); - - checkCancelled(); - const loadedProbe = startLoadedPingProbe(baseUrl); - results.downloadSpeed = await measureDownloadSpeed(baseUrl); - const loadedSamples = await loadedProbe.stop(); - - // DNS timing captured from the most recent resource entry to our server - results.dnsLookupMs = extractDnsMs(baseUrl); - - checkCancelled(); - results.uploadSpeed = await measureUploadSpeed(baseUrl); - checkCancelled(); - // Wait 1s after upload for router buffers to drain (prevents bufferbloat from inflating ping) - await sendProgress({ status: 'Settling connection...' }); - await new Promise(resolve => setTimeout(resolve, 1000)); - checkCancelled(); - const pingResult = await measurePingAndJitter(baseUrl); - results.ping = pingResult.ping; - results.jitter = pingResult.jitter; - results.packetLoss = pingResult.packetLoss; - - // Bufferbloat = loaded ping median minus idle ping. Graded A-F. - if (loadedSamples.length > 0 && results.ping !== undefined) { - const sorted = [...loadedSamples].sort((a, b) => a - b); - const median = sorted[Math.floor(sorted.length / 2)]; - const delta = Math.max(0, median - results.ping); - results.bufferbloatDelta = delta; - results.bufferbloatGrade = gradeBufferbloat(delta); - } - - results.status = 'Test complete'; - - // Set badge with download speed - if (results.downloadSpeed !== undefined) { - const badgeText = results.downloadSpeed >= 100 - ? `${Math.round(results.downloadSpeed)}` - : results.downloadSpeed.toFixed(1); - await chrome.action.setBadgeText({ text: badgeText }); - await chrome.action.setBadgeBackgroundColor({ color: '#007bff' }); - } - - await sendCompletion(results); - } catch (error: unknown) { - const msg = error instanceof Error ? error.message : 'Unknown error'; - console.error("Test failed:", msg); - results.status = `Test failed: ${msg}`; - await sendError(results.status); - await sendCompletion(results); - } finally { - isTestRunning = false; - testAbortController = null; - } - return results; -} - -// --- Scheduled background tests --- -async function rescheduleTestsFromPrefs(): Promise { - try { - const { scheduleMinutes } = await chrome.storage.sync.get('scheduleMinutes'); - const minutes = Number(scheduleMinutes) || 0; - await chrome.alarms.clear(SCHEDULE_ALARM_NAME); - if (minutes > 0) { - chrome.alarms.create(SCHEDULE_ALARM_NAME, { periodInMinutes: minutes, delayInMinutes: minutes }); - } - } catch (e) { - console.error('Reschedule failed:', e); - } -} - -function qualityGrade(dl?: number, ul?: number, ping?: number): 'excellent' | 'good' | 'fair' | 'poor' | null { - if (dl === undefined || ul === undefined || ping === undefined) return null; - if (dl >= 50 && ul >= 20 && ping < 30) return 'excellent'; - if (dl >= 20 && ul >= 10 && ping < 60) return 'good'; - if (dl >= 5 && ul >= 2 && ping < 150) return 'fair'; - return 'poor'; -} - -async function maybeNotifyQualityDrop(current: TestResults): Promise { - try { - const { notifyOnDrop } = await chrome.storage.sync.get('notifyOnDrop'); - if (!notifyOnDrop) return; - const grade = qualityGrade(current.downloadSpeed, current.uploadSpeed, current.ping); - if (grade !== 'poor') return; - await chrome.notifications.create(QUALITY_DROP_NOTIFICATION_ID, { - type: 'basic', - iconUrl: chrome.runtime.getURL('images/icons128.png'), - title: 'MeterX — connection quality dropped', - message: `Poor: ${(current.downloadSpeed ?? 0).toFixed(1)} Mbps down, ping ${(current.ping ?? 0).toFixed(0)}ms.`, - priority: 1, - }); - } catch (e) { - console.error('Notify failed:', e); - } -} - -chrome.alarms.onAlarm.addListener(async (alarm) => { - if (alarm.name !== SCHEDULE_ALARM_NAME) return; - const result = await runFullTest(); - if (result.status.startsWith('Test complete')) { - await maybeNotifyQualityDrop(result); - } -}); - -chrome.runtime.onInstalled.addListener(() => { rescheduleTestsFromPrefs(); }); -chrome.runtime.onStartup.addListener(() => { rescheduleTestsFromPrefs(); }); - -// Side Panel — open on action click when popup disabled, plus available via API -if (chrome.sidePanel?.setPanelBehavior) { - chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }).catch(() => { /* old chrome */ }); -} - -chrome.runtime.onMessage.addListener( - (message: { action: string; url?: string }, _sender: chrome.runtime.MessageSender, sendResponse: (response: unknown) => void) => { - if (message.action === "startTest") { - runFullTest().then(result => { - sendResponse({ status: result.status || "Test initiated..." }); - }).catch((error: Error) => { - sendResponse({ status: `Error: ${error.message}` }); - }); - return true; - } - if (message.action === "cancelTest") { - if (testAbortController) { - testAbortController.abort(); - } - sendResponse({ status: "Cancelled" }); - return false; - } - if (message.action === "getServerUrl") { - getServerUrl().then(url => sendResponse({ url })); - return true; - } - if (message.action === "rescheduleTests") { - rescheduleTestsFromPrefs().then(() => sendResponse({ success: true })); - return true; - } - if (message.action === "openSidePanel") { - if (chrome.sidePanel?.open && _sender.tab?.windowId !== undefined) { - chrome.sidePanel.open({ windowId: _sender.tab.windowId }).catch(() => { /* not supported */ }); - } - sendResponse({ success: true }); - return false; - } - if (message.action === "setServerUrl") { - const url = message.url?.trim() || ''; - if (url) { - chrome.storage.sync.set({ serverUrl: url }).then(() => sendResponse({ success: true })); - } else { - chrome.storage.sync.remove('serverUrl').then(() => sendResponse({ success: true })); - } - return true; - } - } -); - -console.log("MeterX Background Service: Ready"); diff --git a/packages/extension/src/background/index.ts b/packages/extension/src/background/index.ts new file mode 100644 index 0000000..a5e4bb1 --- /dev/null +++ b/packages/extension/src/background/index.ts @@ -0,0 +1,119 @@ +/** + * MeterX service worker: runs tests, saves history, schedules, notifies. + * Pages talk to it with chrome.runtime messages (see docs/CODEMAP.md). + * @author ajithakdev (https://github.com/ajithakdev) + * @license AGPL-3.0 + */ + +/// + +import type { BroadcastMessage, HistoryEntry, TestProgress } from '@meterx/shared'; +import { getServerUrl } from '../lib/browser'; +import { saveHistory } from '../lib/history'; +import { getQuality, QUALITY_COLORS } from '../lib/quality'; +import { CancelledError, HttpError, runTest } from './runner'; + +const ALARM = 'meterx-scheduled-test'; +const DROP_NOTIFICATION = 'meterx-quality-drop'; + +type Source = 'user' | 'schedule' | 'shortcut'; + +let controller: AbortController | null = null; +let progress: TestProgress = {}; + +function broadcast(message: BroadcastMessage): void { + chrome.runtime.sendMessage(message).catch(() => { /* no page open */ }); +} + +function report(p: TestProgress): void { + progress = { ...progress, ...p }; + broadcast({ action: 'testProgress', data: p }); +} + +function describeError(e: unknown): string { + if (e instanceof HttpError && e.status === 429) return 'The server is busy. Try again in a minute.'; + if (e instanceof HttpError) return `The server answered with an error (HTTP ${e.status}).`; + if (e instanceof TypeError) return "Can't reach the test server. Check your connection or the server in Settings."; + return e instanceof Error ? e.message : 'The test failed.'; +} + +async function setBadge(entry: HistoryEntry): Promise { + if (entry.downloadSpeed === undefined) return; + const dl = entry.downloadSpeed; + await chrome.action.setBadgeText({ text: dl >= 100 ? String(Math.round(dl)) : dl.toFixed(dl >= 10 ? 0 : 1) }); + const q = getQuality(entry.downloadSpeed, entry.uploadSpeed ?? 0, entry.ping ?? 999); + await chrome.action.setBadgeBackgroundColor({ color: QUALITY_COLORS[q.cls] }); +} + +/** Notify only when a scheduled test drops to Poor from something better (#67). */ +async function maybeNotifyDrop(entry: HistoryEntry, previous?: HistoryEntry): Promise { + const { notifyOnDrop } = await chrome.storage.sync.get('notifyOnDrop'); + if (!notifyOnDrop || entry.downloadSpeed === undefined) return; + const grade = (e: HistoryEntry) => getQuality(e.downloadSpeed ?? 0, e.uploadSpeed ?? 0, e.ping ?? 999).cls; + if (grade(entry) !== 'poor' || (previous && grade(previous) === 'poor')) return; + await chrome.notifications.create(DROP_NOTIFICATION, { + type: 'basic', + iconUrl: chrome.runtime.getURL('images/icons128.png'), + title: 'Your connection dropped to Poor', + message: `${entry.downloadSpeed.toFixed(1)} Mbps down, ${(entry.ping ?? 0).toFixed(0)} ms ping. Open MeterX for details.`, + }); +} + +async function startTest(source: Source): Promise { + if (controller) return; + controller = new AbortController(); + progress = {}; + try { + const results = await runTest(await getServerUrl(), controller.signal, report); + const { serverUrl: _server, ...rest } = results; + const entry: HistoryEntry = { ...rest, timestamp: Date.now() }; + const previous = await saveHistory(entry); + await setBadge(entry); + broadcast({ action: 'testComplete', data: entry }); + if (source === 'schedule') await maybeNotifyDrop(entry, previous); + } catch (e) { + const cancelled = e instanceof CancelledError || controller.signal.aborted; + if (!cancelled) console.error('MeterX test failed:', e); + broadcast({ action: 'testError', error: cancelled ? 'Test cancelled' : describeError(e), cancelled }); + } finally { + controller = null; + progress = {}; + } +} + +/** Creates the alarm only when it is missing or its period changed, so browser restarts don't reset it (#66). */ +async function syncAlarm(force = false): Promise { + const minutes = Number((await chrome.storage.sync.get('scheduleMinutes')).scheduleMinutes) || 0; + const existing = await chrome.alarms.get(ALARM); + if (!minutes) { if (existing) await chrome.alarms.clear(ALARM); return; } + if (!force && existing?.periodInMinutes === minutes) return; + await chrome.alarms.create(ALARM, { periodInMinutes: minutes, delayInMinutes: minutes }); +} + +chrome.alarms.onAlarm.addListener(alarm => { if (alarm.name === ALARM) startTest('schedule'); }); +chrome.runtime.onInstalled.addListener(() => { syncAlarm(); }); +chrome.runtime.onStartup.addListener(() => { syncAlarm(); }); +chrome.storage.onChanged.addListener((changes, area) => { + if (area === 'sync' && 'scheduleMinutes' in changes) syncAlarm(true); +}); +chrome.commands?.onCommand.addListener(command => { if (command === 'run-test') startTest('shortcut'); }); +chrome.notifications?.onClicked.addListener(id => { + if (id === DROP_NOTIFICATION) chrome.tabs.create({ url: chrome.runtime.getURL('sidepanel.html') }); +}); + +chrome.runtime.onMessage.addListener((message: { action?: string }, _sender, sendResponse) => { + switch (message.action) { + case 'startTest': + startTest('user'); + sendResponse({ running: true }); + return false; + case 'cancelTest': + controller?.abort(); + sendResponse({ running: false }); + return false; + case 'getState': + sendResponse({ running: !!controller, progress }); + return false; + } + return false; +}); diff --git a/packages/extension/src/background/measure.test.ts b/packages/extension/src/background/measure.test.ts new file mode 100644 index 0000000..c1d743c --- /dev/null +++ b/packages/extension/src/background/measure.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect } from 'vitest'; +import { toMbps, bytesAtTime, steadyMbps, median, latencyStats, gradeBufferbloat, uploadChunkBytes, backoffMs } from './measure'; + +const opts = { rampMs: 1000, tailMs: 500, durationMs: 8000 }; + +describe('toMbps', () => { + it('uses decimal megabits', () => { + expect(toMbps(125_000_000, 1000)).toBe(1000); // 125 MB in 1 s = 1 Gbps + }); + it('returns 0 for zero time', () => { + expect(toMbps(100, 0)).toBe(0); + }); +}); + +describe('bytesAtTime', () => { + const s = [{ t: 0, bytes: 0 }, { t: 1000, bytes: 1000 }, { t: 2000, bytes: 3000 }]; + it('interpolates between samples', () => { + expect(bytesAtTime(s, 1500)).toBe(2000); + }); + it('returns the exact sample when the window ends on it', () => { + expect(bytesAtTime(s, 1000)).toBe(1000); + }); + it('clamps outside the range', () => { + expect(bytesAtTime(s, -5)).toBe(0); + expect(bytesAtTime(s, 9999)).toBe(3000); + expect(bytesAtTime([], 10)).toBe(0); + }); +}); + +describe('steadyMbps', () => { + it('measures only the steady window', () => { + // 1 MB/s after a slow first second: steady rate is 8 Mbps. + const s = [{ t: 0, bytes: 0 }, { t: 1000, bytes: 100_000 }, { t: 8000, bytes: 7_100_000 }]; + expect(steadyMbps(s, 8000, opts)).toBeCloseTo(8, 5); + }); + it('reads 0, not NaN, when nothing completed', () => { + expect(steadyMbps([{ t: 0, bytes: 0 }], 8000, opts)).toBe(0); + }); + it('falls back to total over elapsed for a short phase', () => { + expect(steadyMbps([{ t: 0, bytes: 0 }, { t: 500, bytes: 500_000 }], 500, opts)).toBeCloseTo(8, 5); + }); +}); + +describe('latencyStats', () => { + it('takes the median and mean consecutive change', () => { + expect(latencyStats([10, 20, 10, 30], 4)).toEqual({ ping: 15, jitter: (10 + 10 + 20) / 3, packetLoss: 0 }); + }); + it('counts lost probes', () => { + const r = latencyStats([20, 22, 24, 26], 5); + expect(r.packetLoss).toBe(20); + expect(r.ping).toBe(23); + }); + it('has zero jitter for a single ping', () => { + expect(latencyStats([40], 1).jitter).toBe(0); + }); + it('median of nothing is NaN', () => { + expect(median([])).toBeNaN(); + }); +}); + +describe('gradeBufferbloat', () => { + it.each([[0, 'A'], [5, 'A'], [30, 'B'], [60, 'C'], [200, 'D'], [201, 'F']] as const)('%d ms -> %s', (d, g) => { + expect(gradeBufferbloat(d)).toBe(g); + }); +}); + +describe('uploadChunkBytes', () => { + it('uses small bodies on slow links so requests finish (#60)', () => { + expect(uploadChunkBytes(10, 3)).toBe(256 * 1024); + expect(uploadChunkBytes(undefined, 3)).toBe(256 * 1024); + }); + it('caps bodies on fast links', () => { + expect(uploadChunkBytes(5000, 3)).toBe(8 * 1024 * 1024); + }); + it('targets about 4 completions per second per stream', () => { + // 300 Mbps over 3 streams = 12.5 MB/s per stream, quarter of that per body. + expect(uploadChunkBytes(300, 3)).toBe(3_125_000); + }); +}); + +describe('backoffMs', () => { + it('doubles and caps', () => { + expect([1, 2, 3, 4, 10].map(backoffMs)).toEqual([250, 500, 1000, 2000, 2000]); + }); +}); diff --git a/packages/extension/src/background/measure.ts b/packages/extension/src/background/measure.ts new file mode 100644 index 0000000..b6ce90b --- /dev/null +++ b/packages/extension/src/background/measure.ts @@ -0,0 +1,88 @@ +/** + * Pure measurement math. No I/O, so it is unit tested (measure.test.ts). + * @license AGPL-3.0 + */ + +import type { Grade } from '@meterx/shared'; + +/** Cumulative bytes received at time t (ms since the phase started). */ +export interface ByteSample { t: number; bytes: number } + +/** Decimal megabits per second, the unit ISPs advertise (#64). */ +export function toMbps(bytes: number, ms: number): number { + return ms > 0 ? (bytes * 8) / (ms * 1000) : 0; +} + +/** Bytes at tTarget by linear interpolation between samples ordered by t. */ +export function bytesAtTime(samples: ByteSample[], tTarget: number): number { + if (samples.length === 0) return 0; + if (tTarget <= samples[0].t) return samples[0].bytes; + for (let i = 1; i < samples.length; i++) { + const b = samples[i]; + if (b.t >= tTarget) { + const a = samples[i - 1]; + return a.bytes + (b.bytes - a.bytes) * ((tTarget - a.t) / (b.t - a.t || 1)); + } + } + return samples[samples.length - 1].bytes; +} + +/** + * Steady-state throughput: drops the ramp-up and the tail, then measures + * the bytes moved in the window between. Falls back to total/elapsed when + * the phase ended too early for a window. + */ +export function steadyMbps(samples: ByteSample[], elapsedMs: number, opts: { rampMs: number; tailMs: number; durationMs: number }): number { + const end = Math.min(opts.durationMs - opts.tailMs, elapsedMs); + const last = samples.length ? samples[samples.length - 1].bytes : 0; + if (end <= opts.rampMs) return toMbps(last, elapsedMs); + return toMbps(bytesAtTime(samples, end) - bytesAtTime(samples, opts.rampMs), end - opts.rampMs); +} + +export function median(values: number[]): number { + if (values.length === 0) return NaN; + const s = [...values].sort((a, b) => a - b); + const mid = s.length >> 1; + return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2; +} + +/** Median ping, mean absolute change between consecutive pings, and % of probes lost. */ +export function latencyStats(latencies: number[], attempts: number): { ping: number; jitter: number; packetLoss: number } { + let diff = 0; + for (let i = 1; i < latencies.length; i++) diff += Math.abs(latencies[i] - latencies[i - 1]); + return { + ping: median(latencies), + jitter: latencies.length > 1 ? diff / (latencies.length - 1) : 0, + packetLoss: attempts > 0 ? ((attempts - latencies.length) / attempts) * 100 : 0, + }; +} + +/** How much latency rises under load. Thresholds follow the Waveform bufferbloat test. */ +export function gradeBufferbloat(deltaMs: number): Grade { + if (deltaMs <= 5) return 'A'; + if (deltaMs <= 30) return 'B'; + if (deltaMs <= 60) return 'C'; + if (deltaMs <= 200) return 'D'; + return 'F'; +} + +const KB = 1024; +const MIN_UPLOAD_CHUNK = 256 * KB; +const MAX_UPLOAD_CHUNK = 8 * 1024 * KB; + +/** + * Upload body size so each stream finishes about 4 requests a second. + * Bytes only count when a request completes, so fixed 5 MB bodies read + * 0 Mbps on slow uplinks (#60). Sized from the download speed, which is + * known by then and is usually the higher of the two. + */ +export function uploadChunkBytes(downloadMbps: number | undefined, streams: number): number { + const perStreamBytesPerSec = ((downloadMbps || 0) * 1e6) / 8 / Math.max(1, streams); + const target = Math.round(perStreamBytesPerSec / 4); + return Math.min(MAX_UPLOAD_CHUNK, Math.max(MIN_UPLOAD_CHUNK, target)); +} + +/** Delay before retrying a failed transfer request: 250 ms, doubling, capped at 2 s (#65). */ +export function backoffMs(failures: number): number { + return Math.min(2000, 250 * 2 ** Math.max(0, failures - 1)); +} diff --git a/packages/extension/src/background/runner.ts b/packages/extension/src/background/runner.ts new file mode 100644 index 0000000..d1c425a --- /dev/null +++ b/packages/extension/src/background/runner.ts @@ -0,0 +1,226 @@ +/** + * One speed test: connect, idle latency, download, upload. + * Runs in the service worker. The math is in measure.ts. + * @author ajithakdev (https://github.com/ajithakdev) + * @license AGPL-3.0 + */ + +/// + +import type { IpInfo, TestProgress, TestResults } from '@meterx/shared'; +import { + type ByteSample, backoffMs, bytesAtTime, gradeBufferbloat, latencyStats, + median, steadyMbps, toMbps, uploadChunkBytes, +} from './measure'; + +const DOWNLOAD_PATH = '/test-file/25MB.bin'; +const DOWNLOAD_STREAMS = 4; +const UPLOAD_STREAMS = 3; +// Multi-stream, fixed-duration phases (the M-Lab / Ookla approach). +// The worker and Express rate limits are sized for this burst; raise them if you add streams. +const MEASURE_DURATION_MS = 8000; +const WINDOW = { rampMs: 1200, tailMs: 500, durationMs: MEASURE_DURATION_MS }; +const PING_COUNT = 10; +const PING_GAP_MS = 100; +const PING_TIMEOUT_MS = 3000; +const LOADED_PING_INTERVAL_MS = 400; +const PROGRESS_EVERY_MS = 200; + +export type Reporter = (progress: TestProgress) => void; + +export class CancelledError extends Error { + constructor() { super('Test cancelled'); this.name = 'CancelledError'; } +} + +export class HttpError extends Error { + constructor(readonly status: number) { super(`Server answered HTTP ${status}`); this.name = 'HttpError'; } +} + +function sleep(ms: number, signal: AbortSignal): Promise { + return new Promise(resolve => { + if (signal.aborted) return resolve(); + const done = () => { clearTimeout(t); signal.removeEventListener('abort', done); resolve(); }; + const t = setTimeout(done, ms); + signal.addEventListener('abort', done, { once: true }); + }); +} + +async function fetchWithTimeout(url: string, init: RequestInit, ms: number, signal: AbortSignal): Promise { + return fetch(url, { cache: 'no-store', ...init, signal: AbortSignal.any([signal, AbortSignal.timeout(ms)]) }); +} + +const bust = (url: string, tag: string) => `${url}${url.includes('?') ? '&' : '?'}t=${Date.now()}_${tag}`; + +/** Fires HEAD /ping on an interval while a transfer runs; the median rise over idle is bufferbloat. */ +function startLoadedProbe(baseUrl: string, signal: AbortSignal): () => Promise { + const samples: number[] = []; + const stop = new AbortController(); + const both = AbortSignal.any([signal, stop.signal]); + const loop = (async () => { + while (!both.aborted) { + const t0 = performance.now(); + try { + const res = await fetchWithTimeout(bust(`${baseUrl}/ping`, 'load'), { method: 'HEAD' }, PING_TIMEOUT_MS, both); + if (res.ok) samples.push(performance.now() - t0); + } catch { /* a lost probe only lowers the sample count */ } + await sleep(LOADED_PING_INTERVAL_MS, both); + } + })(); + return async () => { stop.abort(); await loop; return samples.length ? median(samples) : undefined; }; +} + +/** + * Runs `streams` parallel request loops for MEASURE_DURATION_MS and returns steady-state Mbps. + * `one` performs a single request and calls `credit(bytes)` as bytes move. + */ +async function runStreams( + signal: AbortSignal, + streams: number, + one: (signal: AbortSignal, credit: (bytes: number) => void) => Promise, + onLive: (mbps: number) => void, +): Promise { + const phase = new AbortController(); + const both = AbortSignal.any([signal, phase.signal]); + const timer = setTimeout(() => phase.abort(), MEASURE_DURATION_MS); + const start = performance.now(); + const samples: ByteSample[] = [{ t: 0, bytes: 0 }]; + let total = 0; + let lastLive = 0; + let lastError: unknown; + + const credit = (bytes: number) => { + if (both.aborted) return; + total += bytes; + const t = performance.now() - start; + samples.push({ t, bytes: total }); + if (t - lastLive >= PROGRESS_EVERY_MS && t > 500) { + lastLive = t; + const span = Math.min(1000, t); + onLive(toMbps(total - bytesAtTime(samples, t - span), span)); + } + }; + + const loop = async () => { + let failures = 0; + while (!both.aborted) { + try { + await one(both, credit); + failures = 0; + } catch (e) { + if (both.aborted) break; + lastError = e; + await sleep(backoffMs(++failures), both); + } + } + }; + + try { + await Promise.all(Array.from({ length: streams }, loop)); + } finally { + clearTimeout(timer); + } + if (signal.aborted) throw new CancelledError(); + if (total === 0) throw lastError instanceof Error ? lastError : new Error('No data reached the server'); + return steadyMbps(samples, Math.min(performance.now() - start, MEASURE_DURATION_MS), WINDOW); +} + +async function measureLatency(baseUrl: string, signal: AbortSignal, report: Reporter) { + const latencies: number[] = []; + for (let i = 0; i < PING_COUNT; i++) { + if (signal.aborted) throw new CancelledError(); + const t0 = performance.now(); + try { + const res = await fetchWithTimeout(bust(`${baseUrl}/ping`, `p${i}`), { method: 'HEAD' }, PING_TIMEOUT_MS, signal); + if (res.ok) { + latencies.push(performance.now() - t0); + report({ phase: 'latency', ping: median(latencies) }); + } + } catch { if (signal.aborted) throw new CancelledError(); } + await sleep(PING_GAP_MS, signal); + } + if (signal.aborted) throw new CancelledError(); + if (latencies.length === 0) throw new Error(`All ${PING_COUNT} pings failed`); + return latencyStats(latencies, PING_COUNT); +} + +/** First request of the test: warms the connection, reads DNS time and server-side connection facts. */ +async function connect(baseUrl: string, signal: AbortSignal): Promise<{ info?: IpInfo; dnsMs?: number }> { + performance.clearResourceTimings(); // DNS must come from this test, not an earlier one (#69) + const url = bust(`${baseUrl}/ip`, 'connect'); + let info: IpInfo | undefined; + try { + const res = await fetchWithTimeout(url, {}, 8000, signal); + if (res.status === 429) throw new HttpError(429); + if (res.ok) info = await res.json() as IpInfo; + } catch (e) { + if (signal.aborted) throw new CancelledError(); + if (e instanceof HttpError) throw e; + // Older self-hosted servers have no /ip; /ping decides whether the server is reachable at all. + const ping = await fetchWithTimeout(bust(`${baseUrl}/ping`, 'connect'), { method: 'HEAD' }, 8000, signal); + if (!ping.ok) throw new HttpError(ping.status); + } + const entry = performance.getEntriesByName(url).at(-1) as PerformanceResourceTiming | undefined; + const dns = entry ? entry.domainLookupEnd - entry.domainLookupStart : 0; + return { info, dnsMs: dns > 0 ? dns : undefined }; +} + +export async function runTest(baseUrl: string, signal: AbortSignal, report: Reporter): Promise { + report({ phase: 'connect', status: 'Connecting' }); + const { info, dnsMs } = await connect(baseUrl, signal); + const results: TestResults = { + serverUrl: baseUrl, + dnsLookupMs: dnsMs, + edge: info?.edge, + isp: info?.isp, + ipv4Ok: info?.family === 'ipv4' || undefined, + ipv6Ok: info?.family === 'ipv6' || undefined, + }; + + report({ phase: 'latency', status: 'Measuring latency' }); + Object.assign(results, await measureLatency(baseUrl, signal, report)); + report({ phase: 'latency', ping: results.ping, jitter: results.jitter, packetLoss: results.packetLoss }); + + report({ phase: 'download', status: 'Testing download' }); + let stopProbe = startLoadedProbe(baseUrl, signal); + try { + results.downloadSpeed = await runStreams(signal, DOWNLOAD_STREAMS, async (s, credit) => { + const res = await fetch(bust(`${baseUrl}${DOWNLOAD_PATH}`, 'd'), { cache: 'no-store', signal: s }); + if (!res.ok || !res.body) throw new HttpError(res.status); + const reader = res.body.getReader(); + for (;;) { + const { done, value } = await reader.read(); + if (done) return; + credit(value.byteLength); + } + }, mbps => report({ phase: 'download', downloadSpeed: mbps })); + } finally { + results.loadedPingDown = await stopProbe(); + } + report({ phase: 'download', downloadSpeed: results.downloadSpeed }); + + report({ phase: 'upload', status: 'Testing upload' }); + const payload = new Uint8Array(uploadChunkBytes(results.downloadSpeed, UPLOAD_STREAMS)); + stopProbe = startLoadedProbe(baseUrl, signal); + try { + results.uploadSpeed = await runStreams(signal, UPLOAD_STREAMS, async (s, credit) => { + const res = await fetch(bust(`${baseUrl}/upload`, 'u'), { + method: 'POST', cache: 'no-store', signal: s, + headers: { 'Content-Type': 'application/octet-stream' }, body: payload, + }); + if (!res.ok) throw new HttpError(res.status); + await res.arrayBuffer(); + // fetch() has no upload progress, so bytes count when each body completes. + credit(payload.byteLength); + }, mbps => report({ phase: 'upload', uploadSpeed: mbps })); + } finally { + results.loadedPingUp = await stopProbe(); + } + + const loaded = Math.max(results.loadedPingDown ?? -Infinity, results.loadedPingUp ?? -Infinity); + if (Number.isFinite(loaded) && results.ping !== undefined) { + results.bufferbloatDelta = Math.max(0, loaded - results.ping); + results.bufferbloatGrade = gradeBufferbloat(results.bufferbloatDelta); + } + report({ phase: 'done', uploadSpeed: results.uploadSpeed }); + return results; +} diff --git a/packages/extension/src/chart.ts b/packages/extension/src/chart.ts deleted file mode 100644 index b80d0c2..0000000 --- a/packages/extension/src/chart.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Side-panel trend chart — inline SVG, no deps. - * Shows download (cyan), upload (purple), ping (green) on a shared time axis. - * @license AGPL-3.0 - */ - -import type { HistoryEntry } from '@meterx/shared'; - -const W = 560; -const H = 120; -const PAD_L = 28; -const PAD_R = 8; -const PAD_T = 8; -const PAD_B = 18; - -export function renderChart(el: HTMLElement, history: HistoryEntry[]): void { - const entries = history.slice(0, 60).reverse(); - if (entries.length < 2) { - el.innerHTML = '
Run two tests to see the trend.
'; - return; - } - - const dl = entries.map(e => e.downloadSpeed ?? null); - const ul = entries.map(e => e.uploadSpeed ?? null); - const pi = entries.map(e => e.ping ?? null); - const times = entries.map(e => e.timestamp); - - const speedMax = Math.max(1, ...dl.filter((v): v is number => v !== null), ...ul.filter((v): v is number => v !== null)); - const pingMax = Math.max(1, ...pi.filter((v): v is number => v !== null)); - - const xStep = (W - PAD_L - PAD_R) / (entries.length - 1); - const xAt = (i: number) => PAD_L + i * xStep; - const yAtSpeed = (v: number) => PAD_T + (H - PAD_T - PAD_B) * (1 - v / speedMax); - const yAtPing = (v: number) => PAD_T + (H - PAD_T - PAD_B) * (1 - v / pingMax); - - const path = (vals: (number | null)[], mapY: (v: number) => number): string => { - let d = ''; - let open = false; - vals.forEach((v, i) => { - if (v === null) { open = false; return; } - d += `${open ? 'L' : 'M'}${xAt(i).toFixed(1)},${mapY(v).toFixed(1)} `; - open = true; - }); - return d.trim(); - }; - - const dlPath = path(dl, yAtSpeed); - const ulPath = path(ul, yAtSpeed); - const piPath = path(pi, yAtPing); - - const firstLabel = new Date(times[0]).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); - const lastLabel = new Date(times[times.length - 1]).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); - - el.innerHTML = ` - - - - ${speedMax.toFixed(0)} - 0 - ${firstLabel} - ${lastLabel} - - - - - Down - Up - Ping - - - `; -} diff --git a/packages/extension/src/format.test.ts b/packages/extension/src/format.test.ts deleted file mode 100644 index 69011c0..0000000 --- a/packages/extension/src/format.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { fmt, getTimeAgo } from './format'; - -describe('fmt', () => { - it('formats a number with decimals', () => { - expect(fmt(3.14159, 2)).toBe('3.14'); - }); - it('returns -- for undefined', () => { - expect(fmt(undefined, 1)).toBe('--'); - }); - it('handles zero', () => { - expect(fmt(0, 1)).toBe('0.0'); - }); -}); - -describe('getTimeAgo', () => { - it('says "just now" for recent', () => { - expect(getTimeAgo(new Date(Date.now() - 5000))).toBe('just now'); - }); - it('formats minutes', () => { - expect(getTimeAgo(new Date(Date.now() - 5 * 60 * 1000))).toBe('5m ago'); - }); - it('formats hours', () => { - expect(getTimeAgo(new Date(Date.now() - 3 * 60 * 60 * 1000))).toBe('3h ago'); - }); - it('formats days', () => { - expect(getTimeAgo(new Date(Date.now() - 2 * 24 * 60 * 60 * 1000))).toBe('2d ago'); - }); -}); diff --git a/packages/extension/src/format.ts b/packages/extension/src/format.ts deleted file mode 100644 index 663459d..0000000 --- a/packages/extension/src/format.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Tiny formatting helpers - * @license AGPL-3.0 - */ - -export function fmt(v: number | undefined, d: number): string { - return v !== undefined ? v.toFixed(d) : '--'; -} - -export function getTimeAgo(date: Date): string { - const diff = Date.now() - date.getTime(); - const mins = Math.floor(diff / 60000); - if (mins < 1) return 'just now'; - if (mins < 60) return `${mins}m ago`; - const hours = Math.floor(mins / 60); - if (hours < 24) return `${hours}h ago`; - const days = Math.floor(hours / 24); - return `${days}d ago`; -} diff --git a/packages/extension/src/history.ts b/packages/extension/src/history.ts deleted file mode 100644 index 8977902..0000000 --- a/packages/extension/src/history.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * History storage + rendering - * @license AGPL-3.0 - */ - -/// - -import type { HistoryEntry } from '@meterx/shared'; -import { fmt } from './format'; -import { getQualityClass } from './quality'; - -export const MAX_HISTORY = 200; -export const HISTORY_DISPLAY = 7; - -export async function saveHistory(entry: HistoryEntry): Promise { - const result = await chrome.storage.local.get('history'); - const history: HistoryEntry[] = result.history || []; - history.unshift(entry); - if (history.length > MAX_HISTORY) history.length = MAX_HISTORY; - await chrome.storage.local.set({ history }); -} - -export async function loadHistory(limit = HISTORY_DISPLAY): Promise { - const result = await chrome.storage.local.get('history'); - return (result.history || []).slice(0, limit); -} - -export async function loadAllHistory(): Promise { - const result = await chrome.storage.local.get('history'); - return result.history || []; -} - -export function renderHistory(list: HTMLElement, entries: HistoryEntry[]): void { - if (entries.length === 0) { - list.innerHTML = '
No tests yet
'; - return; - } - list.innerHTML = entries.map(e => { - const d = new Date(e.timestamp); - const t = `${d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} ${d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}`; - const dl = fmt(e.downloadSpeed, 1); - const ul = fmt(e.uploadSpeed, 1); - const qClass = getQualityClass(e.downloadSpeed, e.uploadSpeed, e.ping); - return `
- ${t} - ${dl}${ul} -
`; - }).join(''); -} diff --git a/packages/extension/src/isp.ts b/packages/extension/src/isp.ts deleted file mode 100644 index 74a773f..0000000 --- a/packages/extension/src/isp.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * ISP advertised-plan comparison. - * Stores user's plan in chrome.storage.sync. Renders delivered % vs paid. - * @license AGPL-3.0 - */ - -/// - -import type { IspPlan, HistoryEntry } from '@meterx/shared'; - -const KEY = 'ispPlan'; - -export async function loadIspPlan(): Promise { - const r = await chrome.storage.sync.get(KEY); - return (r[KEY] as IspPlan | undefined) ?? null; -} - -export async function saveIspPlan(plan: IspPlan | null): Promise { - if (plan === null) { - await chrome.storage.sync.remove(KEY); - } else { - await chrome.storage.sync.set({ [KEY]: plan }); - } -} - -export function renderIspComparison(el: HTMLElement, last: HistoryEntry | undefined, plan: IspPlan | null): void { - if (!plan) { el.classList.add('hidden'); return; } - if (!last || last.downloadSpeed === undefined) { el.classList.add('hidden'); return; } - - const dlPct = plan.downloadMbps > 0 ? (last.downloadSpeed / plan.downloadMbps) * 100 : 0; - const ulPct = plan.uploadMbps > 0 && last.uploadSpeed !== undefined ? (last.uploadSpeed / plan.uploadMbps) * 100 : 0; - - const dlCls = cls(dlPct); - const ulCls = cls(ulPct); - - el.classList.remove('hidden'); - el.innerHTML = ` -
- vs plan${plan.providerName ? ` (${escapeHtml(plan.providerName)})` : ''} - ${plan.downloadMbps}↓ / ${plan.uploadMbps}↑ Mbps -
-
-
- Download - ${dlPct.toFixed(0)}% of paid -
- ${last.uploadSpeed !== undefined ? ` -
- Upload - ${ulPct.toFixed(0)}% of paid -
` : ''} -
- `; -} - -function cls(pct: number): string { - if (pct >= 80) return 'ok'; - if (pct >= 50) return 'warn'; - return 'bad'; -} - -function escapeHtml(s: string): string { - return s.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]!)); -} diff --git a/packages/extension/src/lib/browser.test.ts b/packages/extension/src/lib/browser.test.ts new file mode 100644 index 0000000..80d3c39 --- /dev/null +++ b/packages/extension/src/lib/browser.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from 'vitest'; +import { validateServerUrl, normalizeServerUrl } from './browser'; + +describe('validateServerUrl', () => { + it.each(['https://speed.example.com', 'http://localhost:3000', 'http://127.0.0.1:3000', 'http://nas.local'])('accepts %s', (u) => { + expect(validateServerUrl(u)).toBeNull(); + }); + it.each(['http://speed.example.com', 'ftp://x.test', 'not a url', ''])('rejects %s', (u) => { + expect(validateServerUrl(u)).toEqual(expect.any(String)); + }); +}); + +describe('normalizeServerUrl', () => { + it('drops trailing slashes, query and hash (#68)', () => { + expect(normalizeServerUrl('https://host.test/')).toBe('https://host.test'); + expect(normalizeServerUrl(' https://host.test/meterx//?a=1#x ')).toBe('https://host.test/meterx'); + }); +}); diff --git a/packages/extension/src/lib/browser.ts b/packages/extension/src/lib/browser.ts new file mode 100644 index 0000000..c1f9ca3 --- /dev/null +++ b/packages/extension/src/lib/browser.ts @@ -0,0 +1,55 @@ +/** + * Settings and cross-browser helpers used by the service worker and the pages. + * Chromium (Chrome, Edge, Opera) has chrome.sidePanel; Firefox has sidebarAction. + * @license AGPL-3.0 + */ + +/// + +/** Default backend. Also in static/manifest.json host_permissions; change both together. */ +export const DEFAULT_SERVER_URL = 'https://meterx-speedtest.meterx-ajithakdev.workers.dev'; +export const SITE_URL = 'https://meterx.ajithakdev.com'; +export const REPO_URL = 'https://github.com/ajithakdev/meterx-server'; + +/** Returns an error message, or null when the URL is usable (matches optional_host_permissions). */ +export function validateServerUrl(raw: string): string | null { + let u: URL; + try { u = new URL(raw.trim()); } catch { return 'That is not a valid URL.'; } + if (u.protocol === 'https:') return null; + if (u.protocol === 'http:') { + const h = u.hostname; + if (h === 'localhost' || h === '127.0.0.1' || h.endsWith('.local')) return null; + return 'Use https:// for remote servers. Plain http:// works only for localhost, 127.0.0.1 and *.local.'; + } + return 'The URL must start with https:// (or http:// for localhost).'; +} + +/** Origin plus any path prefix, without a trailing slash, so `${base}/ping` never becomes `//ping` (#68). */ +export function normalizeServerUrl(raw: string): string { + const u = new URL(raw.trim()); + return (u.origin + u.pathname).replace(/\/+$/, ''); +} + +export async function getServerUrl(): Promise { + try { + const { serverUrl } = await chrome.storage.sync.get('serverUrl'); + if (typeof serverUrl === 'string' && serverUrl && !validateServerUrl(serverUrl)) return normalizeServerUrl(serverUrl); + } catch { /* storage unavailable */ } + return DEFAULT_SERVER_URL; +} + +interface SidebarAction { open(): Promise } +const firefoxSidebar = (): SidebarAction | undefined => + (globalThis as unknown as { browser?: { sidebarAction?: SidebarAction } }).browser?.sidebarAction; + +export function panelSupported(): boolean { + return !!(chrome.sidePanel?.open || firefoxSidebar()); +} + +/** Must run inside a click handler: both browsers require a user gesture. */ +export async function openPanel(): Promise { + const ff = firefoxSidebar(); + if (ff) return ff.open(); + const win = await chrome.windows.getCurrent(); + if (win.id !== undefined) await chrome.sidePanel.open({ windowId: win.id }); +} diff --git a/packages/extension/src/lib/export-data.test.ts b/packages/extension/src/lib/export-data.test.ts new file mode 100644 index 0000000..b48f1d9 --- /dev/null +++ b/packages/extension/src/lib/export-data.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest'; +import { escapeCsv } from './export-data'; + +describe('escapeCsv', () => { + it('leaves numbers and plain text alone', () => { + expect(escapeCsv(12.5)).toBe('12.5'); + expect(escapeCsv(-3)).toBe('-3'); + expect(escapeCsv('BOM')).toBe('BOM'); + expect(escapeCsv(undefined)).toBe(''); + }); + it('quotes commas, quotes and both line endings', () => { + expect(escapeCsv('a,b')).toBe('"a,b"'); + expect(escapeCsv('say "hi"')).toBe('"say ""hi"""'); + expect(escapeCsv('a\rb')).toBe('"a\rb"'); + }); + it('defuses formula-leading text (#78)', () => { + expect(escapeCsv('=HYPERLINK("x")')).toBe('"\'=HYPERLINK(""x"")"'); + expect(escapeCsv('@SUM(A1)')).toBe("'@SUM(A1)"); + }); +}); diff --git a/packages/extension/src/export-data.ts b/packages/extension/src/lib/export-data.ts similarity index 79% rename from packages/extension/src/export-data.ts rename to packages/extension/src/lib/export-data.ts index fb30970..91bfa90 100644 --- a/packages/extension/src/export-data.ts +++ b/packages/extension/src/lib/export-data.ts @@ -3,7 +3,7 @@ * @license AGPL-3.0 */ -import type { HistoryEntry } from '@meterx/shared'; +import { VERSION, type HistoryEntry } from '@meterx/shared'; function triggerDownload(blob: Blob, filename: string): void { const url = URL.createObjectURL(blob); @@ -20,7 +20,7 @@ export function exportJson(history: HistoryEntry[]): void { const payload = { exportedAt: new Date().toISOString(), tool: 'MeterX', - version: '2026.0.319-MR1.0', + version: VERSION, count: history.length, results: history.map(e => ({ timestamp: e.timestamp, @@ -35,7 +35,10 @@ export function exportJson(history: HistoryEntry[]): void { dnsLookupMs: e.dnsLookupMs, ipv4Ok: e.ipv4Ok, ipv6Ok: e.ipv6Ok, + loadedPingDownMs: e.loadedPingDown, + loadedPingUpMs: e.loadedPingUp, edge: e.edge, + isp: e.isp, })), }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); @@ -43,7 +46,7 @@ export function exportJson(history: HistoryEntry[]): void { } export function exportCsv(history: HistoryEntry[]): void { - const cols = ['timestamp', 'isoTime', 'downloadMbps', 'uploadMbps', 'pingMs', 'jitterMs', 'packetLossPct', 'bufferbloatDeltaMs', 'bufferbloatGrade', 'dnsLookupMs', 'ipv4Ok', 'ipv6Ok', 'edge']; + const cols = ['timestamp', 'isoTime', 'downloadMbps', 'uploadMbps', 'pingMs', 'jitterMs', 'packetLossPct', 'bufferbloatDeltaMs', 'bufferbloatGrade', 'dnsLookupMs', 'ipv4Ok', 'ipv6Ok', 'edge', 'isp']; const rows = history.map(e => [ e.timestamp, new Date(e.timestamp).toISOString(), @@ -58,15 +61,17 @@ export function exportCsv(history: HistoryEntry[]): void { e.ipv4Ok ?? '', e.ipv6Ok ?? '', e.edge ?? '', + e.isp ?? '', ]); - const csv = [cols.join(','), ...rows.map(r => r.map(escapeCsv).join(','))].join('\n'); + const csv = [cols.join(','), ...rows.map(r => r.map(escapeCsv).join(','))].join('\r\n'); const blob = new Blob([csv], { type: 'text/csv' }); triggerDownload(blob, `meterx-export-${new Date().toLocaleDateString('en-CA')}.csv`); } -function escapeCsv(v: unknown): string { +/** RFC 4180 quoting, plus a leading ' on text that a spreadsheet would run as a formula (#78). */ +export function escapeCsv(v: unknown): string { if (v === null || v === undefined) return ''; - const s = String(v); - if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`; - return s; + let s = String(v); + if (typeof v === 'string' && /^[=+\-@\t\r]/.test(s)) s = `'${s}`; + return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; } diff --git a/packages/extension/src/export-pdf.ts b/packages/extension/src/lib/export-pdf.ts similarity index 95% rename from packages/extension/src/export-pdf.ts rename to packages/extension/src/lib/export-pdf.ts index f3f77ca..e9614b3 100644 --- a/packages/extension/src/export-pdf.ts +++ b/packages/extension/src/lib/export-pdf.ts @@ -3,8 +3,9 @@ * @license AGPL-3.0 */ -import type { HistoryEntry } from '@meterx/shared'; +import { VERSION, type HistoryEntry } from '@meterx/shared'; import { getQuality } from './quality'; +import { consistency, trendPercent } from './report-stats'; interface ConnInfo { type?: string; effectiveType?: string } @@ -14,7 +15,6 @@ export async function exportPdfReport(hist: HistoryEntry[]): Promise { const avg = (a: number[]) => a.length ? a.reduce((s, v) => s + v, 0) / a.length : 0; const mn = (a: number[]) => a.length ? Math.min(...a) : 0; const mx = (a: number[]) => a.length ? Math.max(...a) : 0; - const sd = (a: number[]) => { const m = avg(a); return Math.sqrt(avg(a.map(v => (v - m) ** 2))); }; const dlV = hist.filter(e => e.downloadSpeed !== undefined).map(e => e.downloadSpeed!); const ulV = hist.filter(e => e.uploadSpeed !== undefined).map(e => e.uploadSpeed!); @@ -24,13 +24,13 @@ export async function exportPdfReport(hist: HistoryEntry[]): Promise { const aDl = avg(dlV), aUl = avg(ulV), aPi = avg(piV), aJi = avg(jiV), aLo = avg(loV); const qual = getQuality(aDl, aUl, aPi); - const cons = dlV.length > 1 ? Math.max(0, Math.min(100, 100 - (sd(dlV) / aDl) * 100)) : 100; + const cons = consistency(dlV); const consLabel = cons >= 85 ? 'Stable' : cons >= 70 ? 'Moderate' : cons >= 50 ? 'Unstable' : 'Very unstable'; const anomThresh = aDl * 0.5; const anomCount = dlV.filter(v => v < anomThresh).length; - const halfIdx = Math.floor(dlV.length / 2); - const trendPct = avg(dlV.slice(halfIdx)) > 0 ? ((avg(dlV.slice(0, halfIdx)) - avg(dlV.slice(halfIdx))) / avg(dlV.slice(halfIdx))) * 100 : 0; - const trendLabel = Math.abs(trendPct) < 5 ? 'Stable' : trendPct > 0 ? `Improving (+${trendPct.toFixed(0)}%)` : `Declining (${trendPct.toFixed(0)}%)`; + const trend = trendPercent(dlV); + const trendPct = trend ?? 0; + const trendLabel = trend === null ? 'Not enough data' : Math.abs(trend) < 5 ? 'Stable' : trend > 0 ? `Improving (+${trend.toFixed(0)}%)` : `Declining (${trend.toFixed(0)}%)`; const now = new Date(); const oldest = new Date(hist[hist.length - 1].timestamp); @@ -279,7 +279,7 @@ ${envParts.length > 0 ? `
${envParts.join(' · ')}
` doc.line(margin, footerY - 4, pdfW - margin, footerY - 4); doc.setFontSize(7); doc.setTextColor('#555570'); - doc.text('github.com/ajithakdev/meterx-server | v2026.0.319 | AGPL-3.0', pdfW / 2, footerY, { align: 'center' }); + doc.text(`github.com/ajithakdev/meterx-server | v${VERSION} | AGPL-3.0`, pdfW / 2, footerY, { align: 'center' }); doc.setTextColor('#8888a0'); doc.text(`${p} / ${totalPages}`, pdfW - margin, footerY, { align: 'right' }); if (p > 1) { diff --git a/packages/extension/src/lib/format.test.ts b/packages/extension/src/lib/format.test.ts new file mode 100644 index 0000000..f238224 --- /dev/null +++ b/packages/extension/src/lib/format.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; +import { fmt, fmtSpeed, fmtWhen, getTimeAgo } from './format'; + +describe('fmt', () => { + it('formats a number with decimals', () => { + expect(fmt(3.14159, 2)).toBe('3.14'); + }); + it('returns an em dash for missing or NaN', () => { + expect(fmt(undefined, 1)).toBe('—'); + expect(fmt(NaN, 1)).toBe('—'); + }); + it('handles zero', () => { + expect(fmt(0, 1)).toBe('0.0'); + }); +}); + +describe('getTimeAgo', () => { + it('says "just now" for recent', () => { + expect(getTimeAgo(new Date(Date.now() - 5000))).toBe('just now'); + }); + it('formats minutes', () => { + expect(getTimeAgo(new Date(Date.now() - 5 * 60 * 1000))).toBe('5m ago'); + }); + it('formats hours', () => { + expect(getTimeAgo(new Date(Date.now() - 3 * 60 * 60 * 1000))).toBe('3h ago'); + }); + it('formats days', () => { + expect(getTimeAgo(new Date(Date.now() - 2 * 24 * 60 * 60 * 1000))).toBe('2d ago'); + }); +}); + +describe('fmtSpeed', () => { + it('drops decimals at 100 Mbps and above', () => { + expect(fmtSpeed(84.26)).toBe('84.3'); + expect(fmtSpeed(512.7)).toBe('513'); + }); +}); + +describe('fmtWhen', () => { + it('names today and yesterday', () => { + const now = new Date(2026, 9, 22, 15, 0).getTime(); + expect(fmtWhen(new Date(2026, 9, 22, 9, 5).getTime(), now)).toMatch(/^Today /); + expect(fmtWhen(new Date(2026, 9, 21, 23, 59).getTime(), now)).toMatch(/^Yesterday /); + expect(fmtWhen(new Date(2026, 9, 3, 18, 40).getTime(), now)).not.toMatch(/^(Today|Yesterday)/); + }); +}); diff --git a/packages/extension/src/lib/format.ts b/packages/extension/src/lib/format.ts new file mode 100644 index 0000000..e15740f --- /dev/null +++ b/packages/extension/src/lib/format.ts @@ -0,0 +1,34 @@ +/** + * Formatting helpers. + * @license AGPL-3.0 + */ + +/** Fixed decimals, or an em dash for a missing value. */ +export function fmt(v: number | undefined, d: number): string { + return v !== undefined && Number.isFinite(v) ? v.toFixed(d) : '—'; +} + +/** Speeds: one decimal under 100 Mbps, whole numbers above, so the figure stays short. */ +export function fmtSpeed(v: number | undefined): string { + return fmt(v, v !== undefined && v >= 100 ? 0 : 1); +} + +export function getTimeAgo(date: Date): string { + const mins = Math.floor((Date.now() - date.getTime()) / 60000); + if (mins < 1) return 'just now'; + if (mins < 60) return `${mins}m ago`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h ago`; + return `${Math.floor(hours / 24)}d ago`; +} + +/** "Today 14:05", "Yesterday 09:12", "3 Oct 18:40". */ +export function fmtWhen(ts: number, now = Date.now()): string { + const d = new Date(ts); + const time = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); + const day = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime(); + const diff = Math.round((day(new Date(now)) - day(d)) / 86_400_000); + if (diff === 0) return `Today ${time}`; + if (diff === 1) return `Yesterday ${time}`; + return `${d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })} ${time}`; +} diff --git a/packages/extension/src/lib/history.ts b/packages/extension/src/lib/history.ts new file mode 100644 index 0000000..3049df1 --- /dev/null +++ b/packages/extension/src/lib/history.ts @@ -0,0 +1,28 @@ +/** + * Test history in chrome.storage.local (newest first). Written only by the service worker. + * @license AGPL-3.0 + */ + +/// + +import type { HistoryEntry } from '@meterx/shared'; + +export const MAX_HISTORY = 200; + +export async function loadHistory(limit = MAX_HISTORY): Promise { + const { history } = await chrome.storage.local.get('history'); + return (Array.isArray(history) ? history as HistoryEntry[] : []).slice(0, limit); +} + +/** Prepends the entry and returns the previous newest entry. */ +export async function saveHistory(entry: HistoryEntry): Promise { + const history = await loadHistory(); + const previous = history[0]; + history.unshift(entry); + await chrome.storage.local.set({ history: history.slice(0, MAX_HISTORY) }); + return previous; +} + +export async function clearHistory(): Promise { + await chrome.storage.local.remove('history'); +} diff --git a/packages/extension/src/lib/isp.ts b/packages/extension/src/lib/isp.ts new file mode 100644 index 0000000..071bdf4 --- /dev/null +++ b/packages/extension/src/lib/isp.ts @@ -0,0 +1,26 @@ +/** + * The user's advertised ISP plan, in chrome.storage.sync. + * @license AGPL-3.0 + */ + +/// + +import type { IspPlan } from '@meterx/shared'; + +const KEY = 'ispPlan'; + +export async function loadIspPlan(): Promise { + const r = await chrome.storage.sync.get(KEY); + const plan = r[KEY] as IspPlan | undefined; + return plan && (plan.downloadMbps > 0 || plan.uploadMbps > 0) ? plan : null; +} + +export async function saveIspPlan(plan: IspPlan | null): Promise { + if (plan && (plan.downloadMbps > 0 || plan.uploadMbps > 0)) await chrome.storage.sync.set({ [KEY]: plan }); + else await chrome.storage.sync.remove(KEY); +} + +/** Share of the plan delivered, or null when that side of the plan was left blank (#70). */ +export function planShare(measured: number | undefined, planned: number): number | null { + return measured === undefined || !(planned > 0) ? null : (measured / planned) * 100; +} diff --git a/packages/extension/src/quality.test.ts b/packages/extension/src/lib/quality.test.ts similarity index 74% rename from packages/extension/src/quality.test.ts rename to packages/extension/src/lib/quality.test.ts index 41d4ea9..8f688fc 100644 --- a/packages/extension/src/quality.test.ts +++ b/packages/extension/src/lib/quality.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { getQuality, getQualityClass, scoreUseCases } from './quality'; +import { getQuality, qualityOf, scoreUseCases } from './quality'; describe('getQuality', () => { it('grades excellent for 100/50/10ms', () => { @@ -19,12 +19,13 @@ describe('getQuality', () => { }); }); -describe('getQualityClass', () => { - it('returns q-fair for undefined inputs', () => { - expect(getQualityClass(undefined, undefined, undefined)).toBe('q-fair'); +describe('qualityOf', () => { + it('returns null when a measurement is missing', () => { + expect(qualityOf({ downloadSpeed: 100, uploadSpeed: undefined, ping: 10 })).toBeNull(); + expect(qualityOf(undefined)).toBeNull(); }); - it('prefixes q-', () => { - expect(getQualityClass(100, 50, 10)).toBe('q-excellent'); + it('grades a complete entry', () => { + expect(qualityOf({ downloadSpeed: 100, uploadSpeed: 50, ping: 10 })?.cls).toBe('excellent'); }); }); diff --git a/packages/extension/src/quality.ts b/packages/extension/src/lib/quality.ts similarity index 62% rename from packages/extension/src/quality.ts rename to packages/extension/src/lib/quality.ts index 1d8f81d..6033792 100644 --- a/packages/extension/src/quality.ts +++ b/packages/extension/src/lib/quality.ts @@ -1,9 +1,9 @@ /** - * Quality scoring — shared between popup + PDF export + * Quality grade and use-case readiness. The one copy of these rules (#67). * @license AGPL-3.0 */ -import type { Quality } from '@meterx/shared'; +import type { HistoryEntry, Quality } from '@meterx/shared'; export interface QualityLabel { label: string; @@ -17,11 +17,20 @@ export function getQuality(dl: number, ul: number, ping: number): QualityLabel { return { label: 'Poor', cls: 'poor' }; } -export function getQualityClass(dl?: number, ul?: number, ping?: number): string { - if (dl === undefined || ul === undefined || ping === undefined) return 'q-fair'; - return 'q-' + getQuality(dl, ul, ping).cls; +/** Grade of a stored result, or null when it is missing a measurement. */ +export function qualityOf(e: Pick | undefined): QualityLabel | null { + if (!e || e.downloadSpeed === undefined || e.uploadSpeed === undefined || e.ping === undefined) return null; + return getQuality(e.downloadSpeed, e.uploadSpeed, e.ping); } +/** Toolbar badge colours. Kept in sync with the --q-* tokens in static/styles/tokens.css. */ +export const QUALITY_COLORS: Record = { + excellent: '#0f9f6e', + good: '#2f7de1', + fair: '#c27c0e', + poor: '#d64545', +}; + export type UseCaseScore = 'good' | 'okay' | 'poor'; export interface UseCaseScores { diff --git a/packages/extension/src/lib/report-stats.test.ts b/packages/extension/src/lib/report-stats.test.ts new file mode 100644 index 0000000..e9a9c17 --- /dev/null +++ b/packages/extension/src/lib/report-stats.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; +import { consistency, trendPercent } from './report-stats'; + +describe('trendPercent', () => { + it('has no trend for one result (#76)', () => { + expect(trendPercent([50])).toBeNull(); + expect(trendPercent([])).toBeNull(); + }); + it('compares the newer half with the older half', () => { + expect(trendPercent([120, 100])).toBe(20); + expect(trendPercent([80, 80, 100, 100])).toBe(-20); + }); + it('is null when the older half is all zero', () => { + expect(trendPercent([10, 0])).toBeNull(); + }); +}); + +describe('consistency', () => { + it('never returns NaN for zeros', () => { + expect(consistency([0, 0, 0])).toBe(100); + }); + it('is 100 for identical values and lower for spread', () => { + expect(consistency([50, 50])).toBe(100); + expect(consistency([10, 90])).toBeLessThan(50); + }); +}); diff --git a/packages/extension/src/lib/report-stats.ts b/packages/extension/src/lib/report-stats.ts new file mode 100644 index 0000000..226dbb6 --- /dev/null +++ b/packages/extension/src/lib/report-stats.ts @@ -0,0 +1,26 @@ +/** + * Pure stats for the PDF report, split out so the edge cases are tested. + * @license AGPL-3.0 + */ + +const avg = (a: number[]) => (a.length ? a.reduce((s, v) => s + v, 0) / a.length : 0); + +/** 0 to 100, higher is steadier. 100 for fewer than two results or an all-zero history. */ +export function consistency(values: number[]): number { + const m = avg(values); + if (values.length < 2 || m === 0) return 100; + const sd = Math.sqrt(avg(values.map(v => (v - m) ** 2))); + return Math.max(0, Math.min(100, 100 - (sd / m) * 100)); +} + +/** + * Percent change of the newer half against the older half (values are newest first). + * Null when there is not enough data to compare (#76). + */ +export function trendPercent(newestFirst: number[]): number | null { + if (newestFirst.length < 2) return null; + const half = Math.floor(newestFirst.length / 2); + const newer = avg(newestFirst.slice(0, half)); + const older = avg(newestFirst.slice(half)); + return older > 0 ? ((newer - older) / older) * 100 : null; +} diff --git a/packages/extension/src/lib/tooltips.ts b/packages/extension/src/lib/tooltips.ts new file mode 100644 index 0000000..2553575 --- /dev/null +++ b/packages/extension/src/lib/tooltips.ts @@ -0,0 +1,44 @@ +/** + * Plain-language explanations for metric names, from [data-tip] attributes. + * The text is linked with aria-describedby, so screen readers still read the value first (#73). + * @license AGPL-3.0 + */ + +export const TOOLTIP_COPY: Record = { + download: 'How fast data reaches you. Higher means faster pages and video.', + upload: 'How fast you can send data. Matters for calls and backups.', + ping: 'Time for a round trip to the server when the line is quiet. Under 30 ms feels instant.', + jitter: 'How much ping changes from moment to moment. Low means steady calls.', + 'packet-loss': 'Share of test requests that never came back. Over 1% causes stutters.', + bufferbloat: 'How much ping rises while the line is busy. A is smooth, F means lag during downloads.', +}; + +export function bindTooltips(root: ParentNode): void { + let tip: HTMLElement | null = null; + const hide = () => { if (tip) tip.hidden = true; }; + root.querySelectorAll('[data-tip]').forEach((el, i) => { + const text = TOOLTIP_COPY[el.dataset.tip!]; + if (!text) return; + const desc = document.createElement('span'); + desc.id = `tip-${i}`; + desc.className = 'sr-only'; + desc.textContent = text; + el.append(desc); + el.setAttribute('aria-describedby', desc.id); + if (!el.hasAttribute('tabindex')) el.tabIndex = 0; + const show = () => { + tip ??= Object.assign(document.body.appendChild(document.createElement('div')), { className: 'mx-tooltip', role: 'presentation' }); + tip.textContent = text; + tip.hidden = false; + const r = el.getBoundingClientRect(); + const w = tip.offsetWidth; + tip.style.left = `${Math.max(8, Math.min(window.innerWidth - w - 8, r.left + r.width / 2 - w / 2))}px`; + tip.style.top = `${r.bottom + 6}px`; + }; + el.addEventListener('mouseenter', show); + el.addEventListener('focus', show); + el.addEventListener('mouseleave', hide); + el.addEventListener('blur', hide); + }); + document.addEventListener('keydown', e => { if (e.key === 'Escape') hide(); }); +} diff --git a/packages/extension/src/lib/ui.ts b/packages/extension/src/lib/ui.ts new file mode 100644 index 0000000..6d49742 --- /dev/null +++ b/packages/extension/src/lib/ui.ts @@ -0,0 +1,116 @@ +/** + * Rendering shared by the popup and the side panel. DOM only, no storage. + * @license AGPL-3.0 + */ + +import type { Grade, HistoryEntry, IspPlan } from '@meterx/shared'; +import { fmt } from './format'; +import { qualityOf, scoreUseCases, type UseCaseScore, type UseCaseScores } from './quality'; + +/** A gauge: three-quarter dial with the needle past the middle. Drawn in currentColor. */ +export const MARK_SVG = ``; + +const EDGE_NAMES: Record = { + BOM: 'Mumbai', MAA: 'Chennai', DEL: 'Delhi', BLR: 'Bengaluru', HYD: 'Hyderabad', CCU: 'Kolkata', + AMD: 'Ahmedabad', COK: 'Kochi', NAG: 'Nagpur', PNQ: 'Pune', SIN: 'Singapore', HKG: 'Hong Kong', + NRT: 'Tokyo', KIX: 'Osaka', ICN: 'Seoul', SYD: 'Sydney', MEL: 'Melbourne', LHR: 'London', + FRA: 'Frankfurt', CDG: 'Paris', AMS: 'Amsterdam', MAD: 'Madrid', MXP: 'Milan', IAD: 'Washington', + EWR: 'Newark', SJC: 'San Jose', LAX: 'Los Angeles', SEA: 'Seattle', ORD: 'Chicago', DFW: 'Dallas', + ATL: 'Atlanta', MIA: 'Miami', YYZ: 'Toronto', GRU: 'São Paulo', JNB: 'Johannesburg', DXB: 'Dubai', + KUL: 'Kuala Lumpur', BKK: 'Bangkok', CGK: 'Jakarta', MNL: 'Manila', DAC: 'Dhaka', CMB: 'Colombo', KTM: 'Kathmandu', +}; + +export function edgeName(code?: string): string | undefined { + return code ? (EDGE_NAMES[code] ? `${EDGE_NAMES[code]} (${code})` : code) : undefined; +} + +/** "Jio · tested via Mumbai (BOM)" */ +export function scopeLine(e: HistoryEntry | undefined, custom: boolean): string { + const via = custom ? 'your own server' : edgeName(e?.edge) ? `Cloudflare ${edgeName(e?.edge)}` : 'Cloudflare edge'; + return e?.isp ? `${e.isp} · via ${via}` : `Testing via ${via}`; +} + +/** Sets a figure, or a grey dash at the same size when it is missing. */ +export function setFigure(el: HTMLElement, value: string): void { + el.textContent = value; + el.classList.toggle('dash', value === '—'); +} + +export const USE_CASES: Array<{ key: keyof UseCaseScores; label: string; words: Record }> = [ + { key: 'calls', label: 'Video calls', words: { good: 'Works well', okay: 'May stutter', poor: 'Not enough' } }, + { key: 'stream', label: 'Streaming', words: { good: '4K', okay: 'HD only', poor: 'Not enough' } }, + { key: 'gaming', label: 'Gaming', words: { good: 'Works well', okay: 'May lag', poor: 'Too laggy' } }, + { key: 'upload', label: 'Uploads', words: { good: 'Fast', okay: 'Slow', poor: 'Very slow' } }, +]; + +export function useCasesOf(e: HistoryEntry | undefined): UseCaseScores | null { + if (!e || e.downloadSpeed === undefined || e.uploadSpeed === undefined || e.ping === undefined) return null; + return scoreUseCases(e.downloadSpeed, e.uploadSpeed, e.ping, e.jitter, e.packetLoss); +} + +/** Fills a .ready list, keeping every row in place (a dash when there is no result). */ +export function renderReady(list: HTMLElement, e: HistoryEntry | undefined): void { + const s = useCasesOf(e); + list.innerHTML = USE_CASES.map(u => { + const score = s?.[u.key]; + return `
  • ${u.label}${score ? u.words[score] : '—'}
  • `; + }).join(''); +} + +/** The one-line answer under the figure: share of the plan, then what the connection is good for. */ +export function findingFor(e: HistoryEntry | undefined, plan: IspPlan | null): string { + const s = useCasesOf(e); + if (!e || !s) return 'Run a test to see what this connection can handle.'; + const parts: string[] = []; + if (plan && plan.downloadMbps > 0 && e.downloadSpeed !== undefined) { + parts.push(`${Math.round((e.downloadSpeed / plan.downloadMbps) * 100)}% of your ${plan.downloadMbps} Mbps plan.`); + } + const good = USE_CASES.filter(u => s[u.key] === 'good'); + const worst = USE_CASES.find(u => s[u.key] === 'poor') ?? USE_CASES.find(u => s[u.key] === 'okay'); + if (good.length === USE_CASES.length) parts.push('Ready for calls, 4K, gaming and big uploads.'); + else if (good.length && worst) parts.push(`Good for ${good.map(u => u.label.toLowerCase()).join(', ')}; ${worst.label.toLowerCase()}: ${worst.words[s[worst.key]].toLowerCase()}.`); + else if (worst) parts.push(`${worst.label}: ${worst.words[s[worst.key]].toLowerCase()}. Try closer to the router.`); + return parts.join(' '); +} + +export function setQualityPill(el: HTMLElement, e: HistoryEntry | undefined): void { + const q = qualityOf(e); + el.className = `pill ${q ? `q-${q.cls}` : 'is-empty'}`; + el.textContent = q ? q.label : 'Quality'; +} + +/** Idle vs loaded latency rows; bars share one scale so the rise is visible. */ +export function renderLoad(rows: HTMLElement, gradeEl: HTMLElement, e: Partial | undefined): void { + const vals = [e?.ping, e?.loadedPingDown, e?.loadedPingUp]; + const max = Math.max(1, ...vals.filter((v): v is number => v !== undefined)); + rows.querySelectorAll('.load-row').forEach((row, i) => { + const v = vals[i]; + (row.querySelector('.track > i') as HTMLElement).style.width = v === undefined ? '0%' : `${Math.max(2, (v / max) * 100)}%`; + setFigure(row.querySelector('.v') as HTMLElement, v === undefined ? '—' : `${fmt(v, 0)} ms`); + }); + const g = e?.bufferbloatGrade; + gradeEl.className = `pill ${g ? gradeClass(g) : 'is-empty'}`; + gradeEl.textContent = g ? `Grade ${g}` : 'Grade'; +} + +const gradeClass = (g: Grade) => (g === 'A' || g === 'B' ? 's-good' : g === 'C' ? 's-okay' : 's-poor'); + +/** Wires a button to a floating menu: click toggles, Escape and outside clicks close, focus moves in. */ +export function bindMenu(button: HTMLButtonElement, menu: HTMLElement, onPick: (value: string) => void): void { + const close = () => { menu.hidden = true; button.setAttribute('aria-expanded', 'false'); }; + button.addEventListener('click', (ev) => { + ev.stopPropagation(); + const open = menu.hidden; + menu.hidden = !open; + button.setAttribute('aria-expanded', String(open)); + if (open) menu.querySelector('button')?.focus(); + }); + menu.addEventListener('click', (ev) => { + const item = (ev.target as HTMLElement).closest('button[data-value]'); + if (!item) return; + close(); + onPick(item.dataset.value!); + }); + document.addEventListener('click', (ev) => { if (!menu.contains(ev.target as Node)) close(); }); + document.addEventListener('keydown', (ev) => { if (ev.key === 'Escape' && !menu.hidden) { close(); button.focus(); } }); +} diff --git a/packages/extension/src/options.ts b/packages/extension/src/pages/options.ts similarity index 99% rename from packages/extension/src/options.ts rename to packages/extension/src/pages/options.ts index f944963..73b2eaf 100644 --- a/packages/extension/src/options.ts +++ b/packages/extension/src/pages/options.ts @@ -78,6 +78,7 @@ document.addEventListener('DOMContentLoaded', async () => { urlInput.value = currentUrl; (document.querySelector('input[value="custom"]') as HTMLInputElement).checked = true; customSection.classList.remove('hidden'); + customSection.inert = false; } // Highlight selected radio @@ -94,6 +95,7 @@ document.addEventListener('DOMContentLoaded', async () => { radio.addEventListener('change', () => { const isCustom = radio.value === 'custom'; customSection.classList.toggle('hidden', !isCustom); + customSection.inert = !isCustom; updateRadioStyles(); }); }); diff --git a/packages/extension/src/pages/popup.ts b/packages/extension/src/pages/popup.ts new file mode 100644 index 0000000..2839b2c --- /dev/null +++ b/packages/extension/src/pages/popup.ts @@ -0,0 +1,191 @@ +/** + * Popup: run a test, see the latest result. + * The service worker runs the test and saves history; this page only renders. + * @author ajithakdev (https://github.com/ajithakdev) + * @license AGPL-3.0 + */ + +/// + +import { buildShareUrl, type BroadcastMessage, type HistoryEntry, type Phase, type TestProgress } from '@meterx/shared'; +import { openPanel, panelSupported } from '../lib/browser'; +import { exportCsv, exportJson } from '../lib/export-data'; +import { exportPdfReport } from '../lib/export-pdf'; +import { fmt, fmtSpeed, getTimeAgo } from '../lib/format'; +import { loadHistory } from '../lib/history'; +import { loadIspPlan } from '../lib/isp'; +import { bindTooltips } from '../lib/tooltips'; +import { MARK_SVG, bindMenu, findingFor, renderLoad, renderReady, scopeLine, setFigure, setQualityPill } from '../lib/ui'; + +const $ = (id: string) => document.getElementById(id) as T; + +// Where each phase starts on the progress bar and how long it usually takes. +const PHASE_PLAN: Partial> = { + connect: { from: 0, to: 4, ms: 600 }, + latency: { from: 4, to: 12, ms: 2000 }, + download: { from: 12, to: 56, ms: 8600 }, + upload: { from: 56, to: 100, ms: 8600 }, +}; +const PHASE_ORDER: Phase[] = ['latency', 'download', 'upload']; + +const els = { + scope: $('scope'), download: $('download'), upload: $('upload'), ping: $('ping'), jitter: $('jitter'), loss: $('loss'), + quality: $('quality'), finding: $('finding'), grade: $('grade'), loadRows: $('loadRows'), ready: $('ready'), + progress: $('progress'), fill: $('progressFill'), run: $('runBtn'), status: $('status'), +}; + +let running = false; +let phase: Phase | undefined; +let latest: HistoryEntry | undefined; +let customServer = false; + +function setStatus(text: string, error = false): void { + els.status.textContent = text; + els.status.classList.toggle('is-error', error); +} + +function restingStatus(): void { + setStatus(latest ? `Last test ${getTimeAgo(new Date(latest.timestamp))}` : 'No tests yet'); +} + +function updateRunButton(): void { + els.run.textContent = running ? 'Stop test' : 'Run test'; + // Stop stays available; Start needs a connection (#77). + els.run.disabled = !running && !navigator.onLine; +} + +async function renderResult(e: HistoryEntry | undefined): Promise { + setFigure(els.download, fmtSpeed(e?.downloadSpeed)); + setFigure(els.upload, fmtSpeed(e?.uploadSpeed)); + setFigure(els.ping, fmt(e?.ping, 0)); + setFigure(els.jitter, fmt(e?.jitter, 1)); + setFigure(els.loss, fmt(e?.packetLoss, 0)); + setQualityPill(els.quality, e); + renderLoad(els.loadRows, els.grade, e); + renderReady(els.ready, e); + els.finding.textContent = findingFor(e, await loadIspPlan()); + els.scope.textContent = scopeLine(e, customServer); +} + +function setPhase(p: Phase): void { + if (p === phase) return; + phase = p; + const plan = PHASE_PLAN[p]; + const idx = PHASE_ORDER.indexOf(p); + els.progress.querySelectorAll('.phases li').forEach((li, i) => { + li.classList.toggle('is-active', i === idx); + li.classList.toggle('is-done', idx > i || p === 'done'); + }); + if (!plan) return; + const fill = els.fill; + fill.style.transition = 'none'; + fill.style.width = `${plan.from}%`; + void fill.offsetWidth; // restart the transition from this phase's start + fill.style.transition = `width ${plan.ms}ms linear`; + fill.style.width = `${plan.to}%`; + els.progress.setAttribute('aria-valuenow', String(plan.from)); +} + +function setRunning(on: boolean): void { + running = on; + els.progress.classList.toggle('is-running', on); + if (!on) phase = undefined; + updateRunButton(); +} + +function applyProgress(p: TestProgress): void { + if (p.phase) setPhase(p.phase); + if (p.status) setStatus(`${p.status}…`); + if (p.downloadSpeed !== undefined) setFigure(els.download, fmtSpeed(p.downloadSpeed)); + if (p.uploadSpeed !== undefined) setFigure(els.upload, fmtSpeed(p.uploadSpeed)); + if (p.ping !== undefined) { setFigure(els.ping, fmt(p.ping, 0)); renderLoad(els.loadRows, els.grade, { ping: p.ping }); } + if (p.jitter !== undefined) setFigure(els.jitter, fmt(p.jitter, 1)); + if (p.packetLoss !== undefined) setFigure(els.loss, fmt(p.packetLoss, 0)); +} + +/** Clears figures at the start of a run, keeping every block where it is. */ +function startView(): void { + for (const el of [els.download, els.upload, els.ping, els.jitter, els.loss]) setFigure(el, '—'); + setQualityPill(els.quality, undefined); + renderLoad(els.loadRows, els.grade, undefined); + renderReady(els.ready, undefined); + els.finding.textContent = 'Measuring latency first, then download and upload. About 20 seconds.'; +} + +chrome.runtime.onMessage.addListener((msg: BroadcastMessage) => { + if (msg.action === 'testProgress') { + if (!running) { setRunning(true); startView(); } + applyProgress(msg.data); + } else if (msg.action === 'testComplete') { + setRunning(false); + latest = msg.data; + renderResult(latest); + setStatus('Test complete'); + } else if (msg.action === 'testError') { + setRunning(false); + renderResult(latest); + if (msg.cancelled) restingStatus(); else setStatus(msg.error, true); + } +}); + +document.addEventListener('DOMContentLoaded', async () => { + $('mark').outerHTML = MARK_SVG; + bindTooltips(document); + + customServer = !!(await chrome.storage.sync.get('serverUrl')).serverUrl; + latest = (await loadHistory(1))[0]; + await renderResult(latest); + restingStatus(); + + // A test may already be running (scheduled, shortcut, or started before the popup closed). + const state = await chrome.runtime.sendMessage({ action: 'getState' }).catch(() => null) as { running: boolean; progress: TestProgress } | null; + if (state?.running) { setRunning(true); startView(); applyProgress(state.progress); } + updateRunButton(); + if (!navigator.onLine) setStatus("You're offline", true); + + els.run.addEventListener('click', () => { + if (running) { chrome.runtime.sendMessage({ action: 'cancelTest' }); return; } + setRunning(true); + startView(); + setStatus('Connecting…'); + chrome.runtime.sendMessage({ action: 'startTest' }).catch(() => { + setRunning(false); + setStatus("Couldn't start the test. Reopen MeterX and try again.", true); + }); + }); + + window.addEventListener('online', () => { updateRunButton(); if (!running) restingStatus(); }); + window.addEventListener('offline', () => { updateRunButton(); if (!running) setStatus("You're offline", true); }); + + $('settingsBtn').addEventListener('click', () => chrome.runtime.openOptionsPage()); + $('panelBtn').addEventListener('click', () => { + if (panelSupported()) openPanel().then(() => window.close()).catch(() => chrome.tabs.create({ url: 'sidepanel.html' })); + else chrome.tabs.create({ url: 'sidepanel.html' }); + }); + + $('shareBtn').addEventListener('click', async () => { + if (!latest) { setStatus('Run a test first, then share it'); return; } + const url = buildShareUrl(latest); + try { + await navigator.clipboard.writeText(url); + setStatus('Share link copied'); + } catch { + chrome.tabs.create({ url }); + } + }); + + bindMenu($('exportBtn'), $('exportMenu'), async (kind) => { + const all = await loadHistory(); + if (all.length === 0) { setStatus('Nothing to export yet'); return; } + try { + if (kind === 'pdf') { + setStatus('Building the report…'); + await exportPdfReport(all); + } else if (kind === 'csv') exportCsv(all); + else exportJson(all); + setStatus(`${kind.toUpperCase()} saved`); + } catch (e) { + setStatus(e instanceof Error ? `Export failed: ${e.message}` : 'Export failed', true); + } + }); +}); diff --git a/packages/extension/src/pages/sidepanel.ts b/packages/extension/src/pages/sidepanel.ts new file mode 100644 index 0000000..b736a35 --- /dev/null +++ b/packages/extension/src/pages/sidepanel.ts @@ -0,0 +1,196 @@ +/** + * Side panel: history, trend and the plan comparison. Also opens as a tab + * on browsers without a side panel API. + * @license AGPL-3.0 + */ + +/// + +import type { BroadcastMessage, HistoryEntry, IspPlan } from '@meterx/shared'; +import { exportCsv, exportJson } from '../lib/export-data'; +import { exportPdfReport } from '../lib/export-pdf'; +import { fmt, fmtSpeed, fmtWhen } from '../lib/format'; +import { loadHistory } from '../lib/history'; +import { loadIspPlan, planShare } from '../lib/isp'; +import { qualityOf } from '../lib/quality'; +import { bindTooltips } from '../lib/tooltips'; +import { MARK_SVG, bindMenu, scopeLine, setFigure } from '../lib/ui'; +import { median } from '../background/measure'; + +interface ChartsApi { + line(el: HTMLElement, opts: Record): { legend: HTMLElement | null }; + fmt: { dec(d: number): (v: number) => string }; +} +declare const Charts: ChartsApi; + +const $ = (id: string) => document.getElementById(id) as T; +const ROWS_COLLAPSED = 8; + +let days = 7; +let showAll = false; +let running = false; + +const avg = (a: number[]) => (a.length ? a.reduce((s, v) => s + v, 0) / a.length : undefined); +const values = (rows: HistoryEntry[], k: 'downloadSpeed' | 'uploadSpeed' | 'ping') => + rows.map(r => r[k]).filter((v): v is number => v !== undefined); + +function inRange(all: HistoryEntry[]): HistoryEntry[] { + if (!days) return all; + const since = Date.now() - days * 86_400_000; + return all.filter(e => e.timestamp >= since); +} + +function rangeWords(): string { + return days === 1 ? 'in the last 24 hours' : days ? `in the last ${days} days` : 'in all saved results'; +} + +function finding(rows: HistoryEntry[], plan: IspPlan | null): string { + const dl = values(rows, 'downloadSpeed'); + if (dl.length === 0) return `No tests ${rangeWords()}. Run one to start the history.`; + const mean = avg(dl)!; + const slowest = rows.reduce((a, b) => ((b.downloadSpeed ?? Infinity) < (a.downloadSpeed ?? Infinity) ? b : a)); + let s = `Download averaged ${fmtSpeed(mean)} Mbps over ${dl.length} test${dl.length === 1 ? '' : 's'} ${rangeWords()}`; + const share = plan ? planShare(mean, plan.downloadMbps) : null; + s += share !== null ? `, ${Math.round(share)}% of your ${plan!.downloadMbps} Mbps plan.` : '.'; + if (dl.length > 2 && slowest.downloadSpeed !== undefined && slowest.downloadSpeed < mean * 0.6) { + s += ` Slowest: ${fmtSpeed(slowest.downloadSpeed)} Mbps, ${fmtWhen(slowest.timestamp).toLowerCase()}.`; + } + return s; +} + +function renderChart(rows: HistoryEntry[], plan: IspPlan | null): void { + const chart = $('chart'); + const pts = [...rows].reverse().slice(-120); + const empty = pts.length < 2; + chart.hidden = empty; + $('chartEmpty').hidden = !empty; + $('legend').replaceChildren(); + if (empty) return; + const short = days === 1; + const labels = pts.map(e => { + const d = new Date(e.timestamp); + return short ? d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false }) + : d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' }); + }); + const { legend } = Charts.line(chart, { + labels, + series: [ + { name: 'Download', values: pts.map(e => e.downloadSpeed ?? null) }, + { name: 'Upload', values: pts.map(e => e.uploadSpeed ?? null) }, + ], + format: (v: number) => `${fmtSpeed(v)} Mbps`, + tickFormat: (v: number) => String(Math.round(v)), + height: 200, + reference: plan && plan.downloadMbps > 0 ? { value: plan.downloadMbps, label: `Plan ${plan.downloadMbps}` } : undefined, + }); + if (legend) $('legend').append(legend); +} + +function renderPlan(rows: HistoryEntry[], plan: IspPlan | null): void { + $('planEmpty').hidden = !!plan; + $('planName').textContent = plan?.providerName ?? ''; + const box = $('planRows'); + box.hidden = !plan; + if (!plan) return; + const items = [ + { cls: 'down', label: 'Download', planned: plan.downloadMbps, got: avg(values(rows, 'downloadSpeed')) }, + { cls: 'up', label: 'Upload', planned: plan.uploadMbps, got: avg(values(rows, 'uploadSpeed')) }, + ].filter(i => i.planned > 0); + box.innerHTML = items.map(i => { + const share = planShare(i.got, i.planned); + const scale = Math.max(i.planned, i.got ?? 0) * 1.05; + const width = i.got === undefined ? 0 : (i.got / scale) * 100; + return `
    +
    ${i.label}${fmtSpeed(i.got)} of ${i.planned} Mbps
    + + +
    `; + }).join(''); +} + +function renderRows(rows: HistoryEntry[]): void { + const shown = showAll ? rows : rows.slice(0, ROWS_COLLAPSED); + $('rows').innerHTML = shown.map(e => { + const q = qualityOf(e); + return `${fmtWhen(e.timestamp)}${fmtSpeed(e.downloadSpeed)}` + + `${fmtSpeed(e.uploadSpeed)}${fmt(e.ping, 0)}` + + `${q ? `${q.label}` : '—'}`; + }).join(''); + $('rowsEmpty').hidden = rows.length > 0; + const more = $('moreBtn'); + more.hidden = rows.length <= ROWS_COLLAPSED; + more.textContent = showAll ? 'Show fewer' : `Show all ${rows.length}`; +} + +async function refresh(): Promise { + const [all, plan, { serverUrl }] = await Promise.all([loadHistory(), loadIspPlan(), chrome.storage.sync.get('serverUrl')]); + const rows = inRange(all); + $('scope').textContent = scopeLine(all[0], !!serverUrl); + $('finding').textContent = finding(rows, plan); + setFigure($('avgDown'), fmtSpeed(avg(values(rows, 'downloadSpeed')))); + setFigure($('avgUp'), fmtSpeed(avg(values(rows, 'uploadSpeed')))); + const pings = values(rows, 'ping'); + setFigure($('medPing'), pings.length ? fmt(median(pings), 0) : '—'); + $('count').textContent = String(rows.length); + renderChart(rows, plan); + renderPlan(rows, plan); + renderRows(rows); +} + +function setStatus(text: string, error = false): void { + $('status').textContent = text; + $('status').classList.toggle('is-error', error); +} + +function setRunning(on: boolean): void { + running = on; + $('runBtn').textContent = on ? 'Stop test' : 'Run test'; +} + +document.addEventListener('DOMContentLoaded', async () => { + $('mark').outerHTML = MARK_SVG; + bindTooltips(document); + await refresh(); + + const state = await chrome.runtime.sendMessage({ action: 'getState' }).catch(() => null) as { running: boolean } | null; + setRunning(!!state?.running); + + $('runBtn').addEventListener('click', () => { + chrome.runtime.sendMessage({ action: running ? 'cancelTest' : 'startTest' }); + if (!running) { setRunning(true); setStatus('Testing… about 20 seconds'); } + }); + $('settingsBtn').addEventListener('click', () => chrome.runtime.openOptionsPage()); + $('planSetup').addEventListener('click', () => chrome.runtime.openOptionsPage()); + + $('range').addEventListener('click', (ev) => { + const tab = (ev.target as HTMLElement).closest('button[data-days]'); + if (!tab) return; + days = Number(tab.dataset.days); + showAll = false; + $('range').querySelectorAll('button').forEach(b => b.setAttribute('aria-selected', String(b === tab))); + refresh(); + }); + $('moreBtn').addEventListener('click', () => { showAll = !showAll; refresh(); }); + + bindMenu($('exportBtn'), $('exportMenu'), async (kind) => { + const all = await loadHistory(); + if (all.length === 0) { setStatus('Nothing to export yet'); return; } + try { + if (kind === 'pdf') { setStatus('Building the report…'); await exportPdfReport(all); } + else if (kind === 'csv') exportCsv(all); + else exportJson(all); + setStatus(`${kind.toUpperCase()} saved`); + } catch (e) { + setStatus(e instanceof Error ? `Export failed: ${e.message}` : 'Export failed', true); + } + }); + + chrome.runtime.onMessage.addListener((msg: BroadcastMessage) => { + if (msg.action === 'testProgress' && !running) setRunning(true); + if (msg.action === 'testComplete') { setRunning(false); setStatus(''); } + if (msg.action === 'testError') { setRunning(false); setStatus(msg.cancelled ? '' : msg.error, !msg.cancelled); } + }); + chrome.storage.onChanged.addListener((changes, area) => { + if ((area === 'local' && 'history' in changes) || (area === 'sync' && ('ispPlan' in changes || 'serverUrl' in changes))) refresh(); + }); +}); diff --git a/packages/extension/src/popup.ts b/packages/extension/src/popup.ts deleted file mode 100644 index 71139d6..0000000 --- a/packages/extension/src/popup.ts +++ /dev/null @@ -1,402 +0,0 @@ -/** - * MeterX Popup UI — orchestrator. Heavy logic lives in sibling modules. - * @author ajithakdev (https://github.com/ajithakdev) - * @license AGPL-3.0 - */ - -/// - -import type { HistoryEntry } from '@meterx/shared'; -import { fmt, getTimeAgo } from './format'; -import { getQuality, scoreUseCases } from './quality'; -import { loadHistory, loadAllHistory, saveHistory } from './history'; -import { renderHistory } from './history'; -import { exportPdfReport } from './export-pdf'; -import { exportJson, exportCsv } from './export-data'; -import { buildShareUrl } from './share'; -import { renderSparkline } from './sparkline'; -import { bindTooltips } from './tooltips'; -import { loadIspPlan, renderIspComparison } from './isp'; - -const MAX_SPEED = 200; - -document.addEventListener('DOMContentLoaded', () => { - const $ = (id: string) => document.getElementById(id) as HTMLElement; - - const startButton = $('startButton') as HTMLButtonElement; - const cancelButton = $('cancelButton') as HTMLButtonElement; - const btnText = startButton.querySelector('.btn-text') as HTMLElement; - const btnSpinner = $('btnSpinner'); - const downloadSpeedEl = $('downloadSpeed'); - const uploadSpeedEl = $('uploadSpeed'); - const pingEl = $('ping'); - const jitterEl = $('jitter'); - const packetLossEl = $('packetLoss'); - const statusEl = $('status'); - const heroSpeed = $('heroSpeed'); - const heroLabel = $('heroLabel'); - const heroSection = $('heroSection'); - const speedDisplay = document.querySelector('.speed-display') as HTMLElement; - const progressBar = $('progress-bar'); - const qualityBadge = $('quality-badge'); - const historyToggle = $('historyToggle') as HTMLButtonElement; - const historyLabel = $('historyLabel'); - const historyList = $('historyList'); - const dlBar = $('dlBar'); - const ulBar = $('ulBar'); - const dlIcon = document.querySelector('.dl-icon') as HTMLElement; - const ulIcon = document.querySelector('.ul-icon') as HTMLElement; - const useCases = $('use-cases'); - const ucCalls = $('uc-calls'); - const ucStream = $('uc-stream'); - const ucGaming = $('uc-gaming'); - const ucUpload = $('uc-upload'); - const exportBtn = $('exportBtn') as HTMLButtonElement; - const exportMenu = document.getElementById('exportMenu') as HTMLElement | null; - const exportBackdrop = document.getElementById('exportBackdrop') as HTMLElement | null; - const closeBtn = $('closeBtn') as HTMLButtonElement; - const settingsBtn = $('settingsBtn') as HTMLButtonElement; - const shareBtn = document.getElementById('shareBtn') as HTMLButtonElement | null; - const sparkEl = document.getElementById('sparkline') as HTMLElement | null; - const ispCompareEl = document.getElementById('ispCompare') as HTMLElement | null; - - closeBtn.addEventListener('click', () => window.close()); - settingsBtn.addEventListener('click', () => chrome.runtime.openOptionsPage()); - bindTooltips(document); - - const segments = { - download: $('seg-download'), - upload: $('seg-upload'), - ping: $('seg-ping'), - }; - const lineFills = { - 'dl-ul': $('line-dl-ul'), - 'ul-ping': $('line-ul-ping'), - }; - - // --- Animated number counting --- - let countAnim: number | null = null; - function animateHeroTo(target: number, duration = 400): void { - if (countAnim) cancelAnimationFrame(countAnim); - const start = parseFloat(heroSpeed.textContent || '0') || 0; - const startTime = performance.now(); - function tick(now: number) { - const elapsed = now - startTime; - const progress = Math.min(elapsed / duration, 1); - const eased = 1 - Math.pow(1 - progress, 3); - const current = start + (target - start) * eased; - heroSpeed.textContent = current.toFixed(1); - if (progress < 1) countAnim = requestAnimationFrame(tick); - else countAnim = null; - } - countAnim = requestAnimationFrame(tick); - } - - function setBar(el: HTMLElement, speed: number): void { - const pct = Math.min((speed / MAX_SPEED) * 100, 100); - el.style.width = pct + '%'; - if (pct > 5) el.classList.add('glow'); - } - - function setHero(value: number, label: string): void { - animateHeroTo(value); - heroLabel.textContent = label; - } - - function setHeroText(text: string, label: string): void { - if (countAnim) { cancelAnimationFrame(countAnim); countAnim = null; } - heroSpeed.textContent = text; - heroLabel.textContent = label; - } - - function setSegment(phase: 'download' | 'upload' | 'ping'): void { - const order: Array<'download' | 'upload' | 'ping'> = ['download', 'upload', 'ping']; - const idx = order.indexOf(phase); - order.forEach((seg, i) => { - segments[seg].classList.toggle('active', i === idx); - segments[seg].classList.toggle('done', i < idx); - }); - lineFills['dl-ul'].style.width = idx >= 1 ? '100%' : '0%'; - lineFills['ul-ping'].style.width = idx >= 2 ? '100%' : '0%'; - dlIcon.classList.toggle('active-pulse', phase === 'download'); - ulIcon.classList.toggle('active-pulse', phase === 'upload'); - } - - function applyUseCases(dl?: number, ul?: number, ping?: number, jitter?: number, loss?: number): void { - if (dl === undefined || ul === undefined || ping === undefined) { - useCases.classList.add('hidden'); - return; - } - const s = scoreUseCases(dl, ul, ping, jitter, loss); - ucCalls.className = `uc ${s.calls}`; - ucStream.className = `uc ${s.stream}`; - ucGaming.className = `uc ${s.gaming}`; - ucUpload.className = `uc ${s.upload}`; - useCases.classList.remove('hidden'); - } - - function setTesting(active: boolean): void { - startButton.disabled = active; - cancelButton.classList.toggle('hidden', !active); - progressBar.classList.toggle('hidden', !active); - btnSpinner.classList.toggle('hidden', !active); - heroSection.classList.toggle('testing', active); - btnText.textContent = active ? 'Testing' : 'Start Test'; - if (!active) { - dlIcon.classList.remove('active-pulse'); - ulIcon.classList.remove('active-pulse'); - } - } - - function resetUI(): void { - downloadSpeedEl.textContent = '--'; - uploadSpeedEl.textContent = '--'; - pingEl.textContent = '--'; - jitterEl.textContent = '--'; - packetLossEl.textContent = '--'; - setHeroText('--', 'download'); - dlBar.style.width = '0%'; - ulBar.style.width = '0%'; - dlBar.classList.remove('glow'); - ulBar.classList.remove('glow'); - downloadSpeedEl.classList.remove('has-val'); - uploadSpeedEl.classList.remove('has-val'); - speedDisplay.classList.remove('has-result'); - qualityBadge.classList.add('hidden'); - qualityBadge.className = 'badge hidden'; - useCases.classList.add('hidden'); - Object.values(segments).forEach(s => { s.classList.remove('active', 'done'); }); - lineFills['dl-ul'].style.width = '0%'; - lineFills['ul-ping'].style.width = '0%'; - } - - function checkOnline(): void { - if (!navigator.onLine) { startButton.disabled = true; statusEl.textContent = 'No connection'; } - } - window.addEventListener('online', () => { startButton.disabled = false; statusEl.textContent = 'Connection restored'; }); - window.addEventListener('offline', () => { startButton.disabled = true; statusEl.textContent = 'No connection'; }); - checkOnline(); - - async function refreshAuxiliaryViews(): Promise { - const all = await loadAllHistory(); - if (sparkEl) renderSparkline(sparkEl, all); - if (ispCompareEl) { - const plan = await loadIspPlan(); - renderIspComparison(ispCompareEl, all[0], plan); - } - } - - // --- Last-result restore on popup open --- - (async () => { - const history = await loadAllHistory(); - if (history.length > 0) { - const last = history[0]; - downloadSpeedEl.textContent = fmt(last.downloadSpeed, 1); - uploadSpeedEl.textContent = fmt(last.uploadSpeed, 1); - pingEl.textContent = fmt(last.ping, 1); - jitterEl.textContent = fmt(last.jitter, 1); - packetLossEl.textContent = fmt(last.packetLoss, 0); - - if (last.downloadSpeed !== undefined) { - setHeroText(last.downloadSpeed.toFixed(1), 'download'); - setBar(dlBar, last.downloadSpeed); - downloadSpeedEl.classList.add('has-val'); - speedDisplay.classList.add('has-result'); - } - if (last.uploadSpeed !== undefined) { - setBar(ulBar, last.uploadSpeed); - uploadSpeedEl.classList.add('has-val'); - } - if (last.downloadSpeed !== undefined && last.uploadSpeed !== undefined && last.ping !== undefined) { - const q = getQuality(last.downloadSpeed, last.uploadSpeed, last.ping); - qualityBadge.textContent = q.label; - qualityBadge.className = `badge ${q.cls}`; - applyUseCases(last.downloadSpeed, last.uploadSpeed, last.ping, last.jitter, last.packetLoss); - } - statusEl.textContent = `Last test: ${getTimeAgo(new Date(last.timestamp))}`; - } - await refreshAuxiliaryViews(); - })(); - - startButton.addEventListener('click', () => { - resetUI(); - setTesting(true); - statusEl.textContent = 'Initializing...'; - chrome.runtime.sendMessage({ action: 'startTest' }, (response) => { - if (chrome.runtime.lastError) { - statusEl.textContent = `Error: ${chrome.runtime.lastError.message}`; - setTesting(false); - } else if (response?.status) { /* ack */ } - }); - }); - - cancelButton.addEventListener('click', () => { - chrome.runtime.sendMessage({ action: 'cancelTest' }); - statusEl.textContent = 'Cancelled'; - setTesting(false); - }); - - chrome.runtime.onMessage.addListener((message) => { - if (message.action === 'testProgress') { - const data = message.data; - if (data.status) { - statusEl.textContent = data.status; - if (data.status.includes('latency') || data.status.includes('Ping') || data.status.includes('Pong')) setSegment('ping'); - else if (data.status.includes('Download') || data.status.includes('download')) setSegment('download'); - else if (data.status.includes('Upload') || data.status.includes('upload')) setSegment('upload'); - } - if (data.downloadSpeed !== undefined) { - downloadSpeedEl.textContent = data.downloadSpeed.toFixed(1); - downloadSpeedEl.classList.add('has-val'); - setHero(data.downloadSpeed, 'download'); - setBar(dlBar, data.downloadSpeed); - } - if (data.uploadSpeed !== undefined) { - uploadSpeedEl.textContent = data.uploadSpeed.toFixed(1); - uploadSpeedEl.classList.add('has-val'); - setHero(data.uploadSpeed, 'upload'); - setBar(ulBar, data.uploadSpeed); - } - if (data.ping !== undefined) { - pingEl.textContent = data.ping.toFixed(1); - setHero(data.ping, 'ping'); - } - if (data.jitter !== undefined) jitterEl.textContent = data.jitter.toFixed(1); - if (data.packetLoss !== undefined) packetLossEl.textContent = data.packetLoss.toFixed(0); - } else if (message.action === 'testComplete') { - const data = message.data; - downloadSpeedEl.textContent = fmt(data.downloadSpeed, 1); - uploadSpeedEl.textContent = fmt(data.uploadSpeed, 1); - pingEl.textContent = fmt(data.ping, 1); - jitterEl.textContent = fmt(data.jitter, 1); - packetLossEl.textContent = fmt(data.packetLoss, 0); - - if (data.downloadSpeed !== undefined) { - setBar(dlBar, data.downloadSpeed); - setHero(data.downloadSpeed, 'download'); - downloadSpeedEl.classList.add('has-val'); - speedDisplay.classList.add('has-result'); - } - if (data.uploadSpeed !== undefined) { - setBar(ulBar, data.uploadSpeed); - uploadSpeedEl.classList.add('has-val'); - } - - if (data.downloadSpeed !== undefined && data.uploadSpeed !== undefined && data.ping !== undefined) { - const q = getQuality(data.downloadSpeed, data.uploadSpeed, data.ping); - qualityBadge.textContent = q.label; - qualityBadge.className = `badge ${q.cls}`; - } - applyUseCases(data.downloadSpeed, data.uploadSpeed, data.ping, data.jitter, data.packetLoss); - - statusEl.textContent = data.status || 'Test complete'; - setTesting(false); - Object.values(segments).forEach(s => { s.classList.remove('active'); s.classList.add('done'); }); - lineFills['dl-ul'].style.width = '100%'; - lineFills['ul-ping'].style.width = '100%'; - - const entry: HistoryEntry = { - timestamp: Date.now(), - downloadSpeed: data.downloadSpeed, - uploadSpeed: data.uploadSpeed, - ping: data.ping, - jitter: data.jitter, - packetLoss: data.packetLoss, - bufferbloatDelta: data.bufferbloatDelta, - bufferbloatGrade: data.bufferbloatGrade, - dnsLookupMs: data.dnsLookupMs, - ipv4Ok: data.ipv4Ok, - ipv6Ok: data.ipv6Ok, - edge: data.edge, - }; - saveHistory(entry).then(refreshAuxiliaryViews); - } else if (message.action === 'testError') { - statusEl.textContent = message.error || 'Test failed'; - setTesting(false); - } - }); - - let historyOpen = false; - historyToggle.addEventListener('click', async () => { - historyOpen = !historyOpen; - if (historyOpen) { - const entries = await loadHistory(); - renderHistory(historyList, entries); - historyList.classList.remove('hidden'); - historyLabel.textContent = 'Hide'; - } else { - historyList.classList.add('hidden'); - historyLabel.textContent = 'History'; - } - }); - - // --- Export dispatch --- - async function dispatchExport(kind: 'pdf' | 'json' | 'csv'): Promise { - const all = await loadAllHistory(); - if (all.length === 0) { statusEl.textContent = 'No data to export'; return; } - try { - if (kind === 'pdf') { - statusEl.textContent = 'Generating report...'; - await exportPdfReport(all); - statusEl.textContent = 'Report downloaded'; - } else if (kind === 'json') { - exportJson(all); - statusEl.textContent = 'JSON exported'; - } else { - exportCsv(all); - statusEl.textContent = 'CSV exported'; - } - } catch (e) { - statusEl.textContent = e instanceof Error ? `Export failed: ${e.message}` : 'Export failed'; - } - } - - function closeExportMenu(): void { - exportMenu?.classList.add('hidden'); - exportBackdrop?.classList.add('hidden'); - } - function openExportMenu(): void { - exportMenu?.classList.remove('hidden'); - exportBackdrop?.classList.remove('hidden'); - const first = exportMenu?.querySelector('[data-export]'); - first?.focus(); - } - - exportBtn.addEventListener('click', (e) => { - e.stopPropagation(); - if (exportMenu) { - if (exportMenu.classList.contains('hidden')) openExportMenu(); - else closeExportMenu(); - } else { - dispatchExport('pdf'); - } - }); - - if (exportMenu) { - exportMenu.querySelectorAll('[data-export]').forEach(el => { - el.addEventListener('click', () => { - const kind = el.dataset.export as 'pdf' | 'json' | 'csv'; - closeExportMenu(); - dispatchExport(kind); - }); - }); - exportBackdrop?.addEventListener('click', closeExportMenu); - document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeExportMenu(); }); - } - - // --- Share --- - if (shareBtn) { - shareBtn.addEventListener('click', async () => { - const all = await loadAllHistory(); - const last = all[0]; - if (!last) { statusEl.textContent = 'No result to share'; return; } - const url = buildShareUrl(last); - try { - await navigator.clipboard.writeText(url); - statusEl.textContent = 'Share link copied'; - } catch { - statusEl.textContent = url; - } - }); - } -}); diff --git a/packages/extension/src/share.ts b/packages/extension/src/share.ts deleted file mode 100644 index f1375ad..0000000 --- a/packages/extension/src/share.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Shareable result URL. v1 = hash-encoded payload, no server needed. - * A public viewer page at share target can later decode this fragment. - * @license AGPL-3.0 - */ - -import type { HistoryEntry } from '@meterx/shared'; - -const SHARE_VIEWER = 'https://meterx.dev/share'; - -export function buildShareUrl(entry: HistoryEntry): string { - const compact = { - t: entry.timestamp, - dl: roundOrNull(entry.downloadSpeed, 1), - ul: roundOrNull(entry.uploadSpeed, 1), - p: roundOrNull(entry.ping, 1), - j: roundOrNull(entry.jitter, 1), - l: roundOrNull(entry.packetLoss, 1), - bb: entry.bufferbloatGrade ?? null, - e: entry.edge ?? null, - }; - const encoded = base64UrlEncode(JSON.stringify(compact)); - return `${SHARE_VIEWER}#r=${encoded}`; -} - -export function parseShareUrl(url: string): HistoryEntry | null { - try { - const hash = new URL(url).hash; - const match = hash.match(/r=([A-Za-z0-9_-]+)/); - if (!match) return null; - const json = base64UrlDecode(match[1]); - const c = JSON.parse(json); - return { - timestamp: c.t, - downloadSpeed: c.dl ?? undefined, - uploadSpeed: c.ul ?? undefined, - ping: c.p ?? undefined, - jitter: c.j ?? undefined, - packetLoss: c.l ?? undefined, - bufferbloatGrade: c.bb ?? undefined, - edge: c.e ?? undefined, - }; - } catch { - return null; - } -} - -function roundOrNull(v: number | undefined, d: number): number | null { - if (v === undefined) return null; - return Number(v.toFixed(d)); -} - -function base64UrlEncode(s: string): string { - return btoa(unescape(encodeURIComponent(s))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); -} - -function base64UrlDecode(s: string): string { - const pad = s.length % 4 ? '='.repeat(4 - (s.length % 4)) : ''; - const b64 = (s + pad).replace(/-/g, '+').replace(/_/g, '/'); - return decodeURIComponent(escape(atob(b64))); -} diff --git a/packages/extension/src/sidepanel.ts b/packages/extension/src/sidepanel.ts deleted file mode 100644 index 07dda37..0000000 --- a/packages/extension/src/sidepanel.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * MeterX Side Panel — persistent dashboard next to the browser tab. - * @license AGPL-3.0 - */ - -/// - -import { loadAllHistory, loadHistory, renderHistory } from './history'; -import { renderIspComparison, loadIspPlan } from './isp'; -import { getQuality } from './quality'; -import { fmt } from './format'; -import { bindTooltips } from './tooltips'; -import { renderChart } from './chart'; - -document.addEventListener('DOMContentLoaded', async () => { - const $ = (id: string) => document.getElementById(id) as HTMLElement; - const heroNum = $('heroNum'); - const heroUp = $('heroUp'); - const heroPing = $('heroPing'); - const heroBB = $('heroBB'); - const qualityBadge = $('quality-badge'); - const chartEl = $('chart'); - const historyListEl = $('historyList'); - const ispEl = $('ispCompare'); - const ispEmpty = $('ispEmpty'); - const startBtn = $('startButton'); - const optionsBtn = $('openOptions'); - - bindTooltips(document); - - async function refresh(): Promise { - const all = await loadAllHistory(); - const last = all[0]; - - heroNum.textContent = fmt(last?.downloadSpeed, 1); - heroUp.textContent = fmt(last?.uploadSpeed, 1); - heroPing.textContent = fmt(last?.ping, 1); - heroBB.textContent = last?.bufferbloatGrade ?? '--'; - - if (last && last.downloadSpeed !== undefined && last.uploadSpeed !== undefined && last.ping !== undefined) { - const q = getQuality(last.downloadSpeed, last.uploadSpeed, last.ping); - qualityBadge.textContent = q.label; - qualityBadge.className = `badge ${q.cls}`; - } else { - qualityBadge.classList.add('hidden'); - } - - renderChart(chartEl, all); - renderHistory(historyListEl, await loadHistory(10)); - - const plan = await loadIspPlan(); - if (plan) { - ispEmpty.classList.add('hidden'); - renderIspComparison(ispEl, last, plan); - } else { - ispEl.classList.add('hidden'); - ispEmpty.classList.remove('hidden'); - } - } - - await refresh(); - - startBtn.addEventListener('click', () => { - chrome.runtime.sendMessage({ action: 'startTest' }); - }); - optionsBtn.addEventListener('click', () => chrome.runtime.openOptionsPage()); - - // Live-update when background finishes or history changes - chrome.runtime.onMessage.addListener((msg) => { - if (msg.action === 'testComplete' || msg.action === 'testError') { - setTimeout(refresh, 200); - } - }); - chrome.storage.onChanged.addListener((changes, area) => { - if (area === 'local' && 'history' in changes) refresh(); - if (area === 'sync' && ('ispPlan' in changes)) refresh(); - }); -}); diff --git a/packages/extension/src/sparkline.ts b/packages/extension/src/sparkline.ts deleted file mode 100644 index 11e3002..0000000 --- a/packages/extension/src/sparkline.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Tiny inline SVG sparkline for download-speed trend. - * Pure data -> markup; no runtime deps. - * @license AGPL-3.0 - */ - -import type { HistoryEntry } from '@meterx/shared'; - -const W = 220; -const H = 36; -const PAD = 2; - -export function renderSparkline(el: HTMLElement, history: HistoryEntry[]): void { - const pts = history - .slice(0, 30) - .reverse() - .map(e => e.downloadSpeed) - .filter((v): v is number => v !== undefined); - - if (pts.length < 2) { - el.innerHTML = ''; - el.classList.add('hidden'); - return; - } - el.classList.remove('hidden'); - - const max = Math.max(...pts); - const min = Math.min(...pts); - const range = max - min || 1; - const stepX = (W - PAD * 2) / (pts.length - 1); - - const coords = pts.map((v, i) => { - const x = PAD + i * stepX; - const y = H - PAD - ((v - min) / range) * (H - PAD * 2); - return [x, y] as const; - }); - - const linePath = coords.map(([x, y], i) => `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`).join(' '); - const areaPath = `${linePath} L${(W - PAD).toFixed(1)},${H - PAD} L${PAD},${H - PAD} Z`; - - const latest = pts[pts.length - 1]; - const latestX = coords[coords.length - 1][0].toFixed(1); - const latestY = coords[coords.length - 1][1].toFixed(1); - - el.innerHTML = ` - - - - - - - - - - - -
    ${pts.length} tests${latest.toFixed(1)} Mbps
    - `; -} diff --git a/packages/extension/src/tooltips.ts b/packages/extension/src/tooltips.ts deleted file mode 100644 index 474fc4b..0000000 --- a/packages/extension/src/tooltips.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Plain-language tooltips for jargon (jitter, packet loss, etc.). - * Reads content from [data-tip] attributes on elements. - * @license AGPL-3.0 - */ - -export const TOOLTIP_COPY: Record = { - download: 'Speed coming in. Higher = faster pages.', - upload: 'Speed going out. Matters for calls, backups.', - ping: 'Round-trip delay. <30ms feels instant.', - jitter: 'Ping variation. Low = stable calls.', - 'packet-loss': 'Lost packets. >1% causes stutters.', - quality: 'Overall grade: speed + ping + stability.', - bufferbloat: 'Ping spike under load. A = smooth, F = bad.', - 'use-cases': 'Pass/fail for common activities.', -}; - -interface TipState { - tipEl: HTMLElement | null; - current: HTMLElement | null; -} - -const state: TipState = { tipEl: null, current: null }; - -function ensureTipEl(): HTMLElement { - if (state.tipEl) return state.tipEl; - const el = document.createElement('div'); - el.className = 'mx-tooltip hidden'; - el.setAttribute('role', 'tooltip'); - document.body.appendChild(el); - state.tipEl = el; - return el; -} - -function showTip(anchor: HTMLElement, text: string): void { - const tip = ensureTipEl(); - tip.textContent = text; - tip.classList.remove('hidden'); - const rect = anchor.getBoundingClientRect(); - const tipRect = tip.getBoundingClientRect(); - let left = rect.left + rect.width / 2 - tipRect.width / 2; - left = Math.max(6, Math.min(window.innerWidth - tipRect.width - 6, left)); - const top = rect.bottom + 6; - tip.style.left = `${left}px`; - tip.style.top = `${top}px`; - state.current = anchor; -} - -function hideTip(): void { - if (state.tipEl) state.tipEl.classList.add('hidden'); - state.current = null; -} - -export function bindTooltips(root: ParentNode): void { - const nodes = root.querySelectorAll('[data-tip]'); - nodes.forEach(el => { - const key = el.dataset.tip!; - const text = TOOLTIP_COPY[key] ?? key; - el.setAttribute('aria-label', text); - el.setAttribute('tabindex', el.getAttribute('tabindex') ?? '0'); - el.addEventListener('mouseenter', () => showTip(el, text)); - el.addEventListener('mouseleave', hideTip); - el.addEventListener('focus', () => showTip(el, text)); - el.addEventListener('blur', hideTip); - }); - document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideTip(); }); -} diff --git a/packages/extension/static/fonts/Geist-Variable.woff2 b/packages/extension/static/fonts/Geist-Variable.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..543899b97dbb8f1ca14bd5b530db83bc8376c59d GIT binary patch literal 69492 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 literal 0 HcmV?d00001 diff --git a/packages/extension/static/fonts/OFL.txt b/packages/extension/static/fonts/OFL.txt new file mode 100644 index 0000000..8d003fe --- /dev/null +++ b/packages/extension/static/fonts/OFL.txt @@ -0,0 +1,92 @@ +Copyright (c) 2023 Vercel, in collaboration with basement.studio + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/packages/extension/static/options.css b/packages/extension/static/options.css deleted file mode 100644 index a1226f7..0000000 --- a/packages/extension/static/options.css +++ /dev/null @@ -1,217 +0,0 @@ -:root { - --bg: #0c0c14; - --card: #151520; - --border: #1e1e2e; - --t1: #e8e8e8; - --t2: #8888a0; - --t3: #555570; - --cyan: #00c8ff; - --green: #34d399; - --red: #f87171; - --yellow: #fbbf24; - --purple: #a855f7; - --font: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace; -} - -* { margin: 0; padding: 0; box-sizing: border-box; } - -body { - background: var(--bg); - color: var(--t1); - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - font-size: 14px; - line-height: 1.6; - min-height: 100vh; - display: flex; - justify-content: center; - padding: 28px 20px; -} - -/* Fill near-full viewport but clamp on ultrawide so reading line length stays sane. */ -.container { width: 100%; max-width: 1680px; } - -/* Cards flow in responsive grid that fills wider viewports - instead of a narrow centered column. Status/actions/info stay full-width. */ -.options-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); - gap: 14px; - align-items: start; - margin-bottom: 14px; -} -.options-grid > .card { margin-bottom: 0; } - -@media (max-width: 720px) { - body { padding: 20px 12px; } - .options-grid { grid-template-columns: 1fr; } -} - -.header { - display: flex; - align-items: center; - gap: 14px; - margin-bottom: 24px; -} -.header-text { flex: 1; } - -.close-btn { - background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.08); - color: var(--t3); width: 28px; height: 28px; border-radius: 8px; - display: flex; align-items: center; justify-content: center; - cursor: pointer; transition: all 0.15s; flex-shrink: 0; -} -.close-btn:hover { background: rgba(255,255,255,0.1); color: var(--t1); border-color: rgba(255,255,255,0.15); } - -.logo { - width: 36px; height: 36px; border-radius: 8px; - background: linear-gradient(135deg, #00c8ff, #a855f7); - display: flex; align-items: center; justify-content: center; - font-family: var(--font); font-weight: 800; font-size: 13px; color: #fff; - flex-shrink: 0; -} - -h1 { font-size: 18px; font-weight: 700; color: var(--t1); } -.sub { font-size: 12px; color: var(--t3); } - -/* Cards */ -.card { - background: var(--card); - border: 1px solid var(--border); - border-radius: 10px; - padding: 18px; - margin-bottom: 14px; -} - -label:not(.radio-option) { - font-size: 11px; font-weight: 600; color: var(--t3); - text-transform: uppercase; letter-spacing: 0.06em; - display: block; margin-bottom: 10px; -} - -/* Status Card */ -.status-card { padding: 14px 18px; } - -.status-row { - display: flex; align-items: center; gap: 12px; -} - -.status-dot { - width: 10px; height: 10px; border-radius: 50%; - background: var(--t3); flex-shrink: 0; - transition: background 0.3s; -} -.status-dot.online { background: var(--green); box-shadow: 0 0 8px rgba(52, 211, 153, 0.4); } -.status-dot.offline { background: var(--red); box-shadow: 0 0 8px rgba(248, 113, 113, 0.4); } -.status-dot.checking { background: var(--yellow); animation: pulse-dot 1s infinite; } - -@keyframes pulse-dot { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } -} - -.status-info { flex: 1; } -.status-label { font-size: 13px; font-weight: 600; color: var(--t1); } -.status-detail { font-size: 11px; color: var(--t3); font-family: var(--font); } - -.status-ping { - font-family: var(--font); font-size: 13px; font-weight: 700; - color: var(--cyan); white-space: nowrap; -} - -/* Radio Group */ -.radio-group { - display: flex; flex-direction: column; gap: 8px; -} - -.radio-option { - display: flex; align-items: flex-start; gap: 10px; - padding: 12px 14px; border-radius: 8px; - border: 1px solid var(--border); - cursor: pointer; transition: all 0.15s; - text-transform: none; letter-spacing: 0; font-weight: 400; - margin-bottom: 0; -} -.radio-option:hover { border-color: rgba(255,255,255,0.12); background: rgba(255,255,255,0.02); } -.radio-option.selected { border-color: var(--cyan); background: rgba(0, 200, 255, 0.04); } - -.radio-option input[type="radio"] { - margin-top: 3px; accent-color: var(--cyan); flex-shrink: 0; -} - -.radio-content { display: flex; flex-direction: column; gap: 2px; } -.radio-label { font-size: 13px; font-weight: 600; color: var(--t1); } -.radio-desc { font-size: 11px; color: var(--t3); } - -/* Custom URL */ -.custom-url { - margin-top: 12px; padding-top: 12px; - border-top: 1px solid var(--border); -} -.custom-url.hidden { display: none; } - -.input-row { display: flex; gap: 8px; } - -input[type="url"] { - flex: 1; background: var(--bg); border: 1px solid var(--border); - border-radius: 6px; padding: 8px 12px; color: var(--t1); - font-family: var(--font); font-size: 12px; outline: none; - transition: border-color 0.15s; -} -input[type="url"]:focus { border-color: var(--cyan); } -input[type="url"]::placeholder { color: var(--t3); } - -/* Buttons */ -.btn { - border: none; border-radius: 6px; padding: 8px 18px; - font-size: 12px; font-weight: 600; cursor: pointer; - transition: all 0.15s; white-space: nowrap; -} -.btn-primary { background: var(--cyan); color: #0c0c14; } -.btn-primary:hover { opacity: 0.9; } -.btn-secondary { - background: rgba(255,255,255,0.06); color: var(--t2); - border: 1px solid var(--border); -} -.btn-secondary:hover { background: rgba(255,255,255,0.1); color: var(--t1); } - -.actions { margin-bottom: 20px; } - -/* Test Status */ -.test-status { - margin-top: 8px; font-size: 11px; font-family: var(--font); - padding: 6px 10px; border-radius: 6px; -} -.test-status.success { background: rgba(52,211,153,0.1); color: var(--green); border: 1px solid rgba(52,211,153,0.2); } -.test-status.error { background: rgba(248,113,113,0.1); color: var(--red); border: 1px solid rgba(248,113,113,0.2); } -.test-status.hidden { display: none; } - -/* Info & Footer */ -.info { font-size: 12px; color: var(--t3); line-height: 1.6; margin-bottom: 20px; } -.info p + p { margin-top: 4px; } -.info a { color: var(--cyan); text-decoration: none; } -.info a:hover { text-decoration: underline; } -.footer { font-size: 11px; color: var(--t3); text-align: center; } -.footer a { color: var(--t3); text-decoration: none; transition: color 0.15s; } -.footer a:hover { color: var(--cyan); } - -/* ISP plan card — grid children need min-width:0 so inputs don't push past card edge at zoom */ -.isp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; } -.isp-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; } -.isp-field-wide { grid-column: 1 / -1; } -.isp-fl { font-size: 11px; color: var(--t2); } -.isp-field input { - background: var(--bg); - border: 1px solid var(--border); - border-radius: 6px; - padding: 8px 10px; - color: var(--t1); - font-family: var(--font); - font-size: 12px; - outline: none; - width: 100%; - max-width: 100%; - min-width: 0; - box-sizing: border-box; -} -.isp-field input:focus { border-color: var(--cyan); } -.isp-actions { display: flex; justify-content: flex-end; gap: 8px; } diff --git a/packages/extension/static/options.html b/packages/extension/static/options.html index 9cd8913..2379c7b 100644 --- a/packages/extension/static/options.html +++ b/packages/extension/static/options.html @@ -2,113 +2,112 @@ - MeterX Settings - + + MeterX settings + + + - -
    -
    - -
    -

    MeterX Settings

    -

    Configure your speed test server

    + +
    +
    + +
    +

    MeterX settings

    +
    Server, ISP plan and scheduled tests
    - -
    +
    + +
    + - -
    -
    -
    +
    +
    +
    Checking connection...
    -
    +
    -
    -
    -
    +
    + -
    - -
    - -
    - - + +
    diff --git a/packages/extension/static/popup.css b/packages/extension/static/popup.css deleted file mode 100644 index fa1efb2..0000000 --- a/packages/extension/static/popup.css +++ /dev/null @@ -1,556 +0,0 @@ -/* MeterX — Compact Dark Glass */ - -:root { - --bg: #0b0b12; - --bg-card: rgba(255,255,255,0.03); - --bg-card-h: rgba(255,255,255,0.055); - --border: rgba(255,255,255,0.05); - --t1: #eee; - --t2: rgba(255,255,255,0.50); - --t3: rgba(255,255,255,0.22); - --dl: #00c8ff; - --ul: #a855f7; - --ping: #34d399; - --btn: #3b82f6; - --btn-h: #2563eb; - --danger: #ef4444; - --r: 10px; - --font: 'Inter',system-ui,sans-serif; - --mono: 'JetBrains Mono','SF Mono',monospace; -} - -*,*::before,*::after{box-sizing:border-box;margin:0;padding:0} - -body,html { - font-family: var(--font); - width: 340px; - background: var(--bg); - color: var(--t1); - font-size: 12px; - -webkit-font-smoothing: antialiased; - overflow-x: hidden; -} - -.container { - padding: 14px; - background: var(--bg); - display: flex; - flex-direction: column; - gap: 10px; - animation: fadeSlideIn .25s ease-out; -} - -@keyframes fadeSlideIn { - from { opacity:0; transform:translateY(-6px); } - to { opacity:1; transform:translateY(0); } -} - -/* ── Header ── */ -header { - display: flex; - align-items: center; - justify-content: space-between; - height: 26px; -} -.brand { display:flex; align-items:center; gap:7px; } -.logo { - width:24px;height:22px; - display:flex;align-items:center;justify-content:center; - background:linear-gradient(135deg,var(--dl),var(--ul)); - border-radius:6px; - font-family:var(--mono);font-weight:700;font-size:10px;color:#fff; - letter-spacing:-0.5px; -} -.brand-name { font-weight:700;font-size:13px;letter-spacing:.4px; } - -.header-right { display:flex; align-items:center; gap:6px; } - -.icon-btn { - background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08); - color:var(--t3);font-size:12px;line-height:1;cursor:pointer; - width:22px;height:22px;border-radius:6px;display:flex; - align-items:center;justify-content:center; - transition:all .15s;padding:0; -} -.icon-btn:hover{background:rgba(255,255,255,0.1);color:var(--t1);border-color:rgba(255,255,255,0.15)} - -.close-btn { - background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08); - color:var(--t3);font-size:12px;line-height:1;cursor:pointer; - width:22px;height:22px;border-radius:6px;display:flex; - align-items:center;justify-content:center; - transition:all .15s;font-family:var(--font);padding:0; -} -.close-btn:hover{background:rgba(255,255,255,0.1);color:var(--t1);border-color:rgba(255,255,255,0.15)} - -.badge { - font-size:10px;font-weight:600; - padding:2px 8px;border-radius:100px;letter-spacing:.2px; - transition: all .3s ease; -} -.badge.excellent{background:rgba(52,211,153,.1);color:#34d399;border:1px solid rgba(52,211,153,.18)} -.badge.good{background:rgba(59,130,246,.1);color:#60a5fa;border:1px solid rgba(59,130,246,.18)} -.badge.fair{background:rgba(251,191,36,.1);color:#fbbf24;border:1px solid rgba(251,191,36,.18)} -.badge.poor{background:rgba(239,68,68,.1);color:#f87171;border:1px solid rgba(239,68,68,.18)} - -/* ── Speed Display ── */ -.speed-display { - display: flex; - align-items: center; - gap: 14px; - padding: 12px 14px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--r); - transition: border-color .3s; -} - -.speed-display.has-result { - border-color: rgba(0,200,255,0.08); -} - -.speed-main { - display: flex; - align-items: baseline; - gap: 6px; - flex-shrink: 0; -} - -.speed-number { - font-family: var(--mono); - font-size: 32px; - font-weight: 700; - line-height: 1; - color: var(--t1); - letter-spacing: -1px; - min-width: 80px; - transition: color .3s; -} - -.speed-meta { - display: flex; - flex-direction: column; - gap: 3px; -} - -.speed-unit { - font-size: 10px; - font-weight: 600; - color: var(--t2); - letter-spacing: 0.5px; -} - -.speed-label { - font-size: 9px; - color: var(--t3); - text-transform: uppercase; - letter-spacing: 1px; -} - -/* Mini speed bars */ -.speed-bars { flex:1; display:flex; flex-direction:column; gap:6px; } - -.bar-row { - display: flex; - align-items: center; - gap: 6px; -} - -.bar-icon { - width: 8px; height: 8px; - border-radius: 2px; - flex-shrink: 0; - transition: box-shadow .3s; -} -.dl-icon { background: var(--dl); } -.ul-icon { background: var(--ul); } - -/* Pulse the active bar icon during testing */ -.bar-icon.active-pulse { - animation: icon-pulse 1s ease-in-out infinite; -} -@keyframes icon-pulse { - 0%,100% { box-shadow: 0 0 0 0 transparent; } - 50% { box-shadow: 0 0 6px 1px currentColor; } -} -.dl-icon.active-pulse { color: var(--dl); } -.ul-icon.active-pulse { color: var(--ul); } - -.bar-track { - flex: 1; - height: 4px; - background: rgba(255,255,255,0.06); - border-radius: 2px; - overflow: hidden; -} - -.bar-fill { - height: 100%; - border-radius: 2px; - width: 0%; - transition: width 0.6s cubic-bezier(.4,0,.2,1), box-shadow .6s ease; -} -.dl-fill { background: var(--dl); } -.ul-fill { background: var(--ul); } - -/* Glow intensifies with width */ -.dl-fill.glow { box-shadow: 0 0 8px rgba(0,200,255,0.4), 0 0 2px rgba(0,200,255,0.6); } -.ul-fill.glow { box-shadow: 0 0 8px rgba(168,85,247,0.4), 0 0 2px rgba(168,85,247,0.6); } - -.bar-val { - font-family: var(--mono); - font-size: 11px; - font-weight: 600; - color: var(--t2); - min-width: 32px; - text-align: right; - transition: color .3s; -} - -.bar-val.has-val { color: var(--t1); } - -/* ── Progress Steps ── */ -.steps { - display:flex;align-items:center;justify-content:center; - gap:0;padding:0 10px; -} -.step { - display:flex;align-items:center;gap:4px; - font-size:9px;font-weight:600;color:var(--t3); - text-transform:uppercase;letter-spacing:.5px; - transition:color .25s; -} -.step-dot { - width:5px;height:5px;border-radius:50%; - background:rgba(255,255,255,.08);transition:all .25s; -} -.step-line { - flex:1;height:2px;background:rgba(255,255,255,.05);margin:0 6px; - border-radius:1px;overflow:hidden;position:relative; -} -.step-line-fill { - position:absolute;top:0;left:0; - width:0%;height:100%; - background:var(--ping); - border-radius:1px; - transition:width .4s cubic-bezier(.4,0,.2,1); -} -.step.active{color:var(--dl)} -.step.active .step-dot{background:var(--dl);box-shadow:0 0 6px var(--dl)} -.step.done{color:var(--ping)} -.step.done .step-dot{background:var(--ping)} - -/* ── Stats Row ── */ -.stats { - display: flex; - align-items: center; - justify-content: center; - gap: 0; - padding: 8px 0; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--r); -} - -.stat-item { flex: 1; text-align: center; } - -.stat-val { - display: block; - font-family: var(--mono); - font-size: 14px; - font-weight: 700; - color: var(--t1); - line-height: 1; -} - -.stat-lbl { - font-size: 9px; - color: var(--t3); - text-transform: uppercase; - letter-spacing: .5px; - margin-top: 2px; - display: block; -} - -.stat-u { color: var(--t3); } -.stat-sep { width: 1px; height: 24px; background: var(--border); } -.stat-item:nth-child(1) .stat-val { color: var(--ping); } - -/* ── Action Area ── */ -.action-area { display:flex; flex-direction:column; gap:6px; } - -.action-area #status { - font-size:10px;color:var(--t2);text-align:center;margin:0; - min-height:14px;font-family:var(--font);letter-spacing:.2px; -} - -.action-row { display:flex; gap:6px; } - -#startButton { - flex:1;display:flex;align-items:center;justify-content:center;gap:6px; - background:var(--btn);color:#fff;border:none; - padding:9px 14px; - font-family:var(--font);font-size:12px;font-weight:600; - border-radius:var(--r);cursor:pointer; - transition:all .12s ease;letter-spacing:.2px; -} -#startButton:hover:not(:disabled){ - background:var(--btn-h);transform:translateY(-1px); - box-shadow:0 3px 12px rgba(59,130,246,.3); -} -#startButton:active:not(:disabled){transform:translateY(0);box-shadow:none} -#startButton:disabled{ - background:rgba(255,255,255,.05);color:var(--t3); - cursor:not-allowed;box-shadow:none; -} - -.btn-spinner { - width:12px;height:12px; - border:2px solid rgba(255,255,255,.2); - border-top-color:#fff;border-radius:50%; - animation:spin .6s linear infinite; -} -@keyframes spin{to{transform:rotate(360deg)}} - -.cancel-btn { - background:transparent;color:var(--danger); - border:1px solid rgba(239,68,68,.18); - padding:9px 12px; - font-family:var(--font);font-size:11px;font-weight:600; - border-radius:var(--r);cursor:pointer;transition:all .12s; -} -.cancel-btn:hover{background:rgba(239,68,68,.06);border-color:rgba(239,68,68,.3)} - -/* ── Use-Case Readiness ── */ -.use-cases { - display:flex; - gap:4px; - justify-content:center; -} - -.uc { - display:flex;align-items:center;gap:4px; - font-size:9px;font-weight:500;color:var(--t2); - padding:3px 8px; - background:var(--bg-card); - border:1px solid var(--border); - border-radius:100px; - letter-spacing:.2px; - transition:border-color .2s; -} - -.uc-dot { - width:5px;height:5px;border-radius:50%;flex-shrink:0; - transition:background .3s, box-shadow .3s; -} - -.uc.good .uc-dot{background:var(--ping);box-shadow:0 0 4px rgba(52,211,153,.4)} -.uc.good{border-color:rgba(52,211,153,.15);color:var(--ping)} -.uc.okay .uc-dot{background:#fbbf24;box-shadow:0 0 4px rgba(251,191,36,.4)} -.uc.okay{border-color:rgba(251,191,36,.15);color:#fbbf24} -.uc.poor .uc-dot{background:#f87171;box-shadow:0 0 4px rgba(248,113,113,.4)} -.uc.poor{border-color:rgba(239,68,68,.15);color:#f87171} - -/* ── History ── */ -.history-section { - border-top:1px solid var(--border); - padding-top:8px; -} - -.history-header { - display:flex; - align-items:center; - justify-content:space-between; -} - -.ghost-btn { - display:inline-flex;align-items:center;gap:4px; - background:none;border:none;color:var(--t3); - font-family:var(--font);font-size:10px;font-weight:500; - cursor:pointer;padding:0;transition:color .12s; -} -.ghost-btn:hover{color:var(--t2)} -.ghost-btn svg{opacity:.4} - -.history-list { - margin-top:6px; - display:flex;flex-direction:column;gap:1px; -} - -.history-item { - display:flex;justify-content:space-between;align-items:center; - padding:5px 8px;border-radius:6px; - font-size:10px;color:var(--t2); - transition:background .1s; -} -.history-item:hover{background:var(--bg-card-h)} - -.history-item .h-left { - display:flex;align-items:center;gap:5px; -} - -.quality-dot { - width:5px;height:5px;border-radius:50%;flex-shrink:0; -} -.quality-dot.q-excellent{background:var(--ping)} -.quality-dot.q-good{background:#60a5fa} -.quality-dot.q-fair{background:#fbbf24} -.quality-dot.q-poor{background:#f87171} - -.history-item .speeds { - display:flex;gap:8px; - font-family:var(--mono);font-size:10px;font-weight:500; -} -.history-item .dl{color:var(--dl)} -.history-item .ul{color:var(--ul)} -.history-item .time{color:var(--t3);font-size:9px} - -/* ── Footer ── */ -footer { - display:flex;align-items:center;justify-content:center; - gap:5px;font-size:9px;color:var(--t3);padding-top:2px; -} -.dot{width:2px;height:2px;border-radius:50%;background:var(--t3)} -.footer-link{color:var(--t3);text-decoration:none;transition:color .12s} -.footer-link:hover{color:var(--t2)} - -.heart { - display:inline-block; - font-size:16px; - color:#e04070; - text-shadow:0 0 4px #e04070, 0 0 8px rgba(224,64,112,0.3); - animation:pulse-fade 3s ease-in-out infinite; - margin-left:2px; - vertical-align:middle; - line-height:1; -} -@keyframes pulse-fade { - 0%,100%{opacity:.4;text-shadow:0 0 2px #e04070,0 0 4px rgba(224,64,112,0.2)} - 15%{opacity:1;text-shadow:0 0 6px #e04070,0 0 14px rgba(224,64,112,0.5)} - 30%{opacity:.4;text-shadow:0 0 2px #e04070,0 0 4px rgba(224,64,112,0.2)} -} - -/* ── Utils ── */ -.hidden{display:none !important} - -@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}} -.testing .speed-number{animation:pulse 1.2s ease-in-out infinite} - -/* ── Tooltips ── */ -.mx-tooltip{ - position:fixed; - max-width:220px; - background:#17171f; - color:var(--t1); - border:1px solid rgba(255,255,255,0.08); - border-radius:6px; - padding:8px 10px; - font-size:11px; - line-height:1.4; - box-shadow:0 8px 24px rgba(0,0,0,0.6); - pointer-events:none; - z-index:9999; -} -[data-tip]{cursor:help} -[data-tip]:focus{outline:1px dashed var(--t3);outline-offset:2px} - -/* ── Sparkline ── */ -.sparkline{ - margin:8px 0 6px; - padding:8px 10px; - background:var(--bg-card); - border:1px solid var(--border); - border-radius:var(--r); - display:flex; - flex-direction:column; - gap:2px; -} -.sparkline svg{display:block;width:100%;height:auto} -.spark-meta{ - display:flex; - justify-content:space-between; - font-family:var(--mono); - font-size:9px; - color:var(--t3); - text-transform:uppercase; - letter-spacing:0.04em; -} - -/* ── ISP compare ── */ -.isp-compare{ - margin:6px 0; - padding:8px 10px; - background:var(--bg-card); - border:1px solid var(--border); - border-radius:var(--r); - font-size:11px; -} -.isp-head{ - display:flex; - justify-content:space-between; - color:var(--t2); - margin-bottom:6px; - font-size:10px; - text-transform:uppercase; - letter-spacing:0.04em; -} -.isp-plan{font-family:var(--mono);color:var(--t1)} -.isp-bars{display:flex;flex-direction:column;gap:3px} -.isp-row{display:flex;justify-content:space-between;padding:3px 6px;border-radius:6px;font-family:var(--mono)} -.isp-row.ok{background:rgba(52,211,153,0.10);color:#34d399} -.isp-row.warn{background:rgba(251,191,36,0.10);color:#fbbf24} -.isp-row.bad{background:rgba(248,113,113,0.10);color:#f87171} -.isp-row .isp-k{color:var(--t2);font-family:var(--font)} - -/* ── Export dropdown — floats over popup center to avoid body scroll ── */ -.history-actions{display:flex;gap:10px;align-items:center;line-height:1} -.history-actions > *{display:inline-flex;align-items:center;line-height:1} -.export-wrap{position:relative;display:inline-flex;align-items:center} -.export-backdrop{ - position:fixed; - inset:0; - background:rgba(0,0,0,0.45); - backdrop-filter:blur(2px); - z-index:9998; -} -.export-menu{ - position:fixed; - left:50%; - top:50%; - transform:translate(-50%,-50%); - background:#17171f; - border:1px solid rgba(255,255,255,0.10); - border-radius:10px; - box-shadow:0 12px 32px rgba(0,0,0,0.75); - padding:6px; - min-width:200px; - z-index:9999; -} -.export-menu-title{ - font-size:10px; - text-transform:uppercase; - letter-spacing:0.06em; - color:var(--t3); - padding:6px 10px 4px; -} -.export-menu button{ - display:block; - width:100%; - text-align:left; - padding:10px 12px; - background:none; - border:none; - color:var(--t1); - font-size:12px; - border-radius:6px; - cursor:pointer; - font-family:var(--font); -} -.export-menu button:hover, -.export-menu button:focus{background:var(--bg-card-h);outline:none} -.export-menu .export-sub{ - display:block; - font-size:10px; - color:var(--t3); - margin-top:2px; -} diff --git a/packages/extension/static/popup.html b/packages/extension/static/popup.html index e8b378a..b66f778 100644 --- a/packages/extension/static/popup.html +++ b/packages/extension/static/popup.html @@ -1,142 +1,91 @@ - MeterX - - - - + MeterX + + + - -
    - -
    -
    - - MeterX -
    -
    - - - -
    -
    - - -
    -
    -
    --
    -
    - Mbps - download -
    -
    - -
    -
    - -
    - -- -
    -
    - -
    - -- -
    -
    + +
    + +
    +
    MeterX
    +
    Testing via Cloudflare edge
    - - -
    - - - - - +
    +
    +
    +

    Download

    + Quality +
    +
    + —Mbps +
    +

    Run a test to see what this connection can handle.

    +
    - - +
    +
    Upload
    —Mbps
    +
    Ping
    —ms
    +
    Jitter
    —ms
    +
    Loss
    —%
    +
    - -
    -
    - -- - Ping ms -
    -
    -
    - -- - Jitter ms +
    +
    +

    Latency under load

    + Grade
    -
    -
    - -- - Loss % +
    +
    Idle—
    +
    Downloading—
    +
    Uploading—
    -
    +
    - -
    -

    Ready to test

    -
    - - -
    -
    +
    +

    Good enough for

    +
      +
      +
      - -
      -
      - -
      - -
      - - - +
      +
      + + +
      + +
      +

      No tests yet

      +
      + +
      -
      - - - -
      + diff --git a/packages/extension/static/sidepanel.css b/packages/extension/static/sidepanel.css deleted file mode 100644 index f7f6594..0000000 --- a/packages/extension/static/sidepanel.css +++ /dev/null @@ -1,20 +0,0 @@ -body.sidepanel, html { width: 100%; min-width: 280px; } -.sp-container { display:flex; flex-direction:column; gap:16px; padding:14px; } -.sp-header { display:flex; align-items:center; justify-content:space-between; } -.sp-h2 { font-size:11px; text-transform:uppercase; letter-spacing:0.06em; color:var(--t2); margin-bottom:8px; } -.sp-hero { display:flex; align-items:flex-end; justify-content:space-between; padding:14px; background:var(--bg-card); border:1px solid var(--border); border-radius:var(--r); } -.sp-hero-main { display:flex; align-items:baseline; gap:6px; } -.sp-hero-num { font-family:var(--mono); font-size:38px; font-weight:700; color:var(--dl); } -.sp-hero-unit { font-size:12px; color:var(--t2); } -.sp-hero-side { display:flex; flex-direction:column; gap:2px; font-family:var(--mono); font-size:11px; color:var(--t2); text-align:right; } -.sp-hero-side span { color:var(--t1); } -.sp-chart { width:100%; height:140px; background:var(--bg-card); border:1px solid var(--border); border-radius:var(--r); padding:10px; } -.sp-empty { color:var(--t3); font-size:11px; } -.sp-actions { display:flex; gap:8px; } -.sp-primary, .sp-secondary { flex:1; padding:10px; border-radius:var(--r); border:1px solid var(--border); font-size:12px; cursor:pointer; font-family:var(--font); } -.sp-primary { background:var(--btn); color:#fff; border-color:var(--btn); } -.sp-primary:hover { background:var(--btn-h); } -.sp-secondary { background:var(--bg-card); color:var(--t1); } -.sp-secondary:hover { background:var(--bg-card-h); } -.sp-footer { text-align:center; font-size:10px; color:var(--t3); } -.sp-footer a { color:var(--t2); } diff --git a/packages/extension/static/sidepanel.html b/packages/extension/static/sidepanel.html index 1470bad..2f2f74d 100644 --- a/packages/extension/static/sidepanel.html +++ b/packages/extension/static/sidepanel.html @@ -2,57 +2,94 @@ - MeterX — Network Dashboard - - + + MeterX history + + + - -
      -
      -
      - - MeterX Dashboard + +
      +
      + +
      +
      MeterX
      +
      Testing via Cloudflare edge
      - + +
      -
      -
      - -- - Mbps ↓ +
      +
      +
      +

      Connection history

      +

      No tests yet. Run one to start the history.

      +
      +
      + + + + +
      -
      -
      -- Mbps ↑
      -
      -- ms ping
      -
      -- bufferbloat
      -
      -
      - -
      -

      Trend

      -
      -
      -
      -

      ISP Plan

      - -

      Set your ISP plan in Settings to see how much of what you pay for you are getting.

      -
      +
      +
      Download, average
      —Mbps
      +
      Upload, average
      —Mbps
      +
      Ping, median
      —ms
      +
      Tests
      0
      +
      -
      -

      Recent Tests

      -
      -
      +
      +
      +

      Speed over time

      + +
      +
      +
      + +
      +
      -
      - - -
      +
      +
      +

      Against your plan

      + +
      +
      +
      + +
      +
      - +
      +
      +

      Results

      + +
      +
      + + + +
      WhenDownUpPingQuality
      + + +
      +
      +

      +
      + diff --git a/packages/extension/static/styles/app.css b/packages/extension/static/styles/app.css new file mode 100644 index 0000000..ac105d1 --- /dev/null +++ b/packages/extension/static/styles/app.css @@ -0,0 +1,219 @@ +/* + * MeterX design system (popup, side panel, settings). + * Style: achromatic ledger. The chrome is ink and grey; the one data hue (--data) + * marks download, the main figure; status colour appears only on quality pills. + * Badge colours in src/lib/quality.ts mirror --positive / --warning / --negative. + */ + +@font-face { + font-family: "Geist"; + src: url("../fonts/Geist-Variable.woff2") format("woff2"); + font-weight: 100 900; + font-display: block; +} + +:root { + color-scheme: light dark; + --ground: #f2f4f7; + --surface: #ffffff; + --raised: #eaedf1; + --border: #e1e4e8; + --rule: #eceef1; + --control: #cdd2d8; + --text: #0a0a0a; + --muted: #5b6068; + --data: #1d5fbf; + --data-track: #dbe5f4; + --primary: #141414; + --on-primary: #ffffff; + --positive: #247b50; + --negative: #c4242a; + --warning: #92400e; + --pill-positive-bg: #dcfae5; --pill-positive-ink: #04250e; + --pill-negative-bg: #ffedea; --pill-negative-ink: #831c1e; + --pill-warning-bg: #fff4d6; --pill-warning-ink: #7a3a0b; + --pill-neutral-bg: #eaedf1; --pill-neutral-ink: #3d4148; + --focus: #1d5fbf; + + /* charts.js reads these */ + --chart-1: var(--data); + --chart-2: #8a8f96; + --chart-muted: #8a8f96; + --grid: #e6e8eb; + --axis-ink: #5b6068; + --ink: var(--text); + + --radius-card: 8px; + --radius-control: 6px; + --gap: 12px; + --pad: 16px; + --control-height: 36px; + --fs-figure: 40px; + --fs-kpi: 24px; + --fs-title: 18px; + --fs-section: 14px; + --fs-body: 14px; + --fs-small: 12px; + --font: "Geist", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --ease: cubic-bezier(.2, .7, .2, 1); +} + +@media (prefers-color-scheme: dark) { + :root { + --ground: #0b0c0e; + --surface: #151618; + --raised: #1f2124; + --border: #26292d; + --rule: #1f2124; + --control: #3a3e44; + --text: #f4f5f6; + --muted: #a4a8ae; + --data: #74a6ff; + --data-track: #1b2a44; + --primary: #f4f5f6; + --on-primary: #0b0c0e; + --positive: #4cc38a; + --negative: #f0717a; + --warning: #f0b45a; + --pill-positive-bg: #12291d; --pill-positive-ink: #6fdca5; + --pill-negative-bg: #2e1517; --pill-negative-ink: #ff9aa1; + --pill-warning-bg: #2d2210; --pill-warning-ink: #f4c27a; + --pill-neutral-bg: #1f2124; --pill-neutral-ink: #c9ccd1; + --focus: #74a6ff; + --chart-2: #8d9198; + --chart-muted: #8d9198; + --grid: #26292d; + --axis-ink: #a4a8ae; + } +} + +*, *::before, *::after { box-sizing: border-box; } +html { -webkit-text-size-adjust: 100%; } +body { + margin: 0; + background: var(--ground); + color: var(--text); + font: 400 var(--fs-body)/20px var(--font); + -webkit-font-smoothing: antialiased; +} +h1, h2, h3, p, dl, dd, ul, ol { margin: 0; padding: 0; } +ul, ol { list-style: none; } +a { color: inherit; text-underline-offset: 3px; } +button, input, select { font: inherit; color: inherit; } +:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 4px; } +.num { font-variant-numeric: tabular-nums; } +.muted { color: var(--muted); } +.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } +[hidden] { display: none !important; } + +/* --- Shell bar: mark, name, scope, tools --- */ +.bar { display: flex; align-items: center; gap: 10px; min-height: 52px; } +.mark { width: 24px; height: 24px; flex: none; color: var(--text); } +.who { min-width: 0; flex: 1; } +.who .name { font-weight: 600; font-size: 14px; line-height: 18px; } +.who .scope { color: var(--muted); font-size: var(--fs-small); line-height: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.tools { display: flex; gap: 2px; } + +/* --- Controls --- */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 6px; + height: var(--control-height); padding: 0 14px; border-radius: var(--radius-control); + border: 1px solid var(--control); background: var(--surface); color: var(--text); + font-weight: 500; cursor: pointer; white-space: nowrap; + transition: background-color 120ms var(--ease), transform 80ms var(--ease), border-color 120ms var(--ease); +} +.btn:hover { background: var(--raised); } +.btn:active { transform: scale(.98); } +.btn:disabled { cursor: not-allowed; color: var(--muted); background: var(--raised); } +.btn-primary { background: var(--primary); border-color: var(--primary); color: var(--on-primary); } +.btn-primary:hover { background: color-mix(in srgb, var(--primary) 86%, var(--ground)); } +.btn-primary:disabled { background: var(--raised); border-color: var(--control); color: var(--muted); } +.btn-text { border-color: transparent; background: transparent; padding: 0 8px; color: var(--muted); } +.btn-text:hover { background: var(--raised); color: var(--text); } +.btn-danger { color: var(--negative); border-color: var(--control); } +.btn-wide { width: 100%; } +.icon-btn { + width: 32px; height: 32px; display: inline-grid; place-items: center; border-radius: var(--radius-control); + border: 0; background: transparent; color: var(--muted); cursor: pointer; + transition: background-color 120ms var(--ease), color 120ms var(--ease); +} +.icon-btn:hover { background: var(--raised); color: var(--text); } +.icon-btn svg { width: 18px; height: 18px; } + +.field { display: grid; gap: 8px; } +.field label, .field .label { font-weight: 500; } +.input { + height: 40px; width: 100%; min-width: 0; padding: 0 12px; border-radius: var(--radius-control); + border: 1px solid var(--control); background: var(--surface); color: var(--text); +} +.input::placeholder { color: var(--muted); } +.input:hover { border-color: var(--muted); } +.input[aria-invalid="true"] { border-color: var(--negative); } + +.choice { display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px; border-radius: var(--radius-control); cursor: pointer; } +.choice:hover { background: var(--raised); } +.choice input { margin: 3px 0 0; accent-color: var(--primary); width: 16px; height: 16px; flex: none; } +.choice .t { font-weight: 500; display: block; } +.choice .d { color: var(--muted); display: block; } + +/* --- Pills (status beside a value only) --- */ +.pill { + display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 999px; + font-size: var(--fs-small); font-weight: 500; white-space: nowrap; + background: var(--pill-neutral-bg); color: var(--pill-neutral-ink); +} +.pill.q-excellent, .pill.q-good, .pill.s-good { background: var(--pill-positive-bg); color: var(--pill-positive-ink); } +.pill.q-fair, .pill.s-okay { background: var(--pill-warning-bg); color: var(--pill-warning-ink); } +.pill.q-poor, .pill.s-poor { background: var(--pill-negative-bg); color: var(--pill-negative-ink); } +.pill.is-empty { visibility: hidden; } + +/* --- Cards and strips --- */ +.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-card); } +.card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; padding: 14px var(--pad) 0; } +.card-head h2 { font-size: var(--fs-section); font-weight: 600; line-height: 20px; } +.card-body { padding: 10px var(--pad) 14px; } + +/* One container split by dividers; each cell a label over a figure. */ +.strip { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; } +.strip > div { padding: 12px 14px; min-width: 0; } +.strip > div + div { border-left: 1px solid var(--rule); } +.strip dt { color: var(--muted); font-size: var(--fs-small); line-height: 16px; white-space: nowrap; } +.strip dd { font-size: 20px; line-height: 28px; font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; } +.unit { color: var(--muted); font-size: var(--fs-small); font-weight: 400; margin-left: 2px; } +.dash { color: var(--muted); } + +/* Latency under load: idle vs loaded, the bufferbloat evidence. */ +.load-rows { display: grid; gap: 8px; } +.load-row { display: grid; grid-template-columns: 92px 1fr 56px; align-items: center; gap: 10px; } +.load-row .k { color: var(--muted); } +.load-row .v { text-align: right; font-variant-numeric: tabular-nums; } +.track { height: 6px; border-radius: 3px; background: var(--raised); overflow: hidden; } +.track > i { display: block; height: 100%; width: 0; background: var(--chart-2); border-radius: 3px; transition: width 400ms var(--ease); } +.load-row.idle .track > i { background: var(--text); } + +/* Readiness list */ +.ready { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 12px; } +.ready li { display: flex; justify-content: space-between; align-items: center; gap: 8px; min-width: 0; } +.ready .k { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +/* Floating menu (the one place a shadow is allowed) */ +.menu-wrap { position: relative; } +.menu { + position: absolute; z-index: 10; min-width: 200px; padding: 4px; border-radius: var(--radius-card); + background: var(--surface); border: 1px solid var(--border); + box-shadow: 0 8px 24px rgb(0 0 0 / .14); +} +.menu button { display: block; width: 100%; text-align: left; padding: 8px 10px; border: 0; border-radius: var(--radius-control); background: transparent; cursor: pointer; } +.menu button:hover, .menu button:focus-visible { background: var(--raised); } +.menu small { display: block; color: var(--muted); font-size: var(--fs-small); line-height: 16px; } + +/* Tooltip for plain-language metric explanations */ +.mx-tooltip { + position: fixed; z-index: 20; max-width: 220px; padding: 6px 8px; border-radius: var(--radius-control); + background: var(--primary); color: var(--on-primary); font-size: var(--fs-small); line-height: 16px; + pointer-events: none; box-shadow: 0 4px 16px rgb(0 0 0 / .16); +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { transition-duration: 0ms !important; animation-duration: 0ms !important; } +} diff --git a/packages/extension/static/styles/options.css b/packages/extension/static/styles/options.css new file mode 100644 index 0000000..3b9990f --- /dev/null +++ b/packages/extension/static/styles/options.css @@ -0,0 +1,41 @@ +/* Settings page (opened in a tab). Toggling a choice never moves anything outside it. */ + +.settings .wrap { max-width: 1040px; margin: 0 auto; padding: 4px 16px 32px; } +.settings main { display: grid; gap: var(--gap); } +.settings .bar h1.name { font-size: var(--fs-section); } + +.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: var(--gap); align-items: start; } +.hint { margin-bottom: 10px; } +.choices { display: grid; gap: 2px; margin: 0 -12px; } +.choice.selected .t { color: var(--text); } +.choice.check { margin: 8px -12px 0; align-items: center; } +.choice.check input { margin-top: 0; } + +/* Custom URL stays in the layout when off (dimmed and inert) so the cards below never jump. */ +.custom-url { margin-top: 10px; transition: opacity 120ms var(--ease); } +.custom-url.hidden { opacity: .45; } +.input-row { display: flex; gap: 8px; } +.input-row .btn { height: 40px; } +.test-status { min-height: 20px; margin-top: 6px; font-size: var(--fs-small); line-height: 20px; color: var(--muted); } +.test-status.success { color: var(--positive); } +.test-status.error { color: var(--negative); } + +.isp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } +.isp-grid .wide { grid-column: 1 / -1; } +.row-end { display: flex; justify-content: flex-end; margin-top: 8px; } + +.status-card { display: flex; align-items: center; gap: 12px; padding: 12px var(--pad); } +.status-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--muted); } +.status-dot.online { background: var(--positive); } +.status-dot.offline { background: var(--negative); } +.status-dot.checking { background: var(--warning); animation: mx-pulse 1s ease-in-out infinite; } +.status-info { flex: 1; min-width: 0; } +.status-label, .status-detail { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.status-label { font-weight: 500; } +.status-detail { font-size: var(--fs-small); line-height: 16px; min-height: 16px; } +.status-ping { color: var(--muted); } +@keyframes mx-pulse { 50% { opacity: .35; } } + +.save-row { display: flex; justify-content: flex-end; } +.save-row .btn { min-width: 96px; } +.foot { display: grid; gap: 2px; font-size: var(--fs-small); line-height: 16px; margin-top: 8px; } diff --git a/packages/extension/static/styles/panel.css b/packages/extension/static/styles/panel.css new file mode 100644 index 0000000..bf5c7bb --- /dev/null +++ b/packages/extension/static/styles/panel.css @@ -0,0 +1,61 @@ +/* Side panel (usually 320-480px) and the same page opened in a tab. */ + +.panel .wrap { max-width: 880px; margin: 0 auto; padding: 4px 16px 24px; } +.panel .bar { position: sticky; top: 0; z-index: 5; background: var(--ground); } +.panel .bar .btn { height: 32px; } +.panel main { display: grid; gap: var(--gap); } + +.title-row { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 12px; margin-top: 8px; } +.title-row h1 { font-size: var(--fs-title); line-height: 26px; font-weight: 600; letter-spacing: -0.01em; } +.finding { margin-top: 4px; min-height: 40px; max-width: 60ch; } + +.tabs { display: flex; gap: 2px; padding: 2px; border-radius: var(--radius-control); background: var(--raised); } +.tabs button { + height: 28px; padding: 0 10px; border: 0; border-radius: 4px; background: transparent; color: var(--muted); + font-size: var(--fs-small); font-weight: 500; cursor: pointer; white-space: nowrap; +} +.tabs button:hover { color: var(--text); } +.tabs button[aria-selected="true"] { background: var(--surface); color: var(--text); } + +.strip { grid-auto-flow: row; grid-template-columns: repeat(4, 1fr); } +.strip dd { font-size: 20px; } +.strip .lead { font-size: 28px; line-height: 32px; color: var(--data); letter-spacing: -0.01em; } +.strip .lead.dash { color: var(--muted); } +.strip dd { display: flex; align-items: baseline; min-height: 32px; } +@media (max-width: 420px) { + .strip { grid-template-columns: 1fr 1fr; } + .strip > div:nth-child(3) { border-left: 0; } + .strip > div:nth-child(n + 3) { border-top: 1px solid var(--rule); } +} + +.legend { display: flex; gap: 12px; color: var(--muted); font-size: var(--fs-small); } +.legend > * { display: inline-flex; align-items: center; gap: 6px; } +.chart { min-height: 200px; } +.empty { + display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; + min-height: 40px; padding: 8px 12px; border: 1px dashed var(--control); border-radius: var(--radius-card); color: var(--muted); +} +#chartEmpty { min-height: 200px; justify-content: center; text-align: center; } +.link { color: var(--text); text-decoration: underline; } + +.plan-rows { display: grid; gap: 12px; } +.plan-row .top { display: flex; justify-content: space-between; gap: 8px; } +.plan-row .top b { font-weight: 500; font-variant-numeric: tabular-nums; } +.plan-row .bar-track { position: relative; height: 8px; margin-top: 6px; border-radius: 4px; background: var(--raised); } +.plan-row .bar-track i { position: absolute; inset: 0 auto 0 0; border-radius: 4px; background: var(--text); } +.plan-row.down .bar-track i { background: var(--data); } +.plan-row .bar-track .target { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--text); } +.plan-row .sub { margin-top: 4px; color: var(--muted); font-size: var(--fs-small); } +.plan-row .sub.is-low { color: var(--negative); } + +.results { width: 100%; border-collapse: collapse; } +.results th { text-align: left; color: var(--muted); font-size: var(--fs-small); font-weight: 400; padding: 0 0 6px; } +.results td { padding: 9px 0; border-top: 1px solid var(--rule); white-space: nowrap; } +.results th + th, .results td + td { padding-left: 10px; } +.results .num { text-align: right; font-variant-numeric: tabular-nums; } +.results td.when { color: var(--muted); } +.results td.down { color: var(--data); font-weight: 500; } +.more { margin-top: 6px; } +.status { min-height: 20px; color: var(--muted); font-size: var(--fs-small); } +.status.is-error { color: var(--negative); } +.card-head .menu { right: 0; top: calc(100% + 4px); } diff --git a/packages/extension/static/styles/popup.css b/packages/extension/static/styles/popup.css new file mode 100644 index 0000000..e39caaa --- /dev/null +++ b/packages/extension/static/styles/popup.css @@ -0,0 +1,40 @@ +/* Popup: 360px wide. Every block keeps its size in every state (no layout shift). */ + +body.popup { + width: 360px; + padding: 4px 12px 12px; +} +.popup main { display: grid; gap: 8px; } + +.answer { padding: 14px var(--pad) 14px; } +.answer-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; height: 22px; } +.answer .label { font-size: var(--fs-section); font-weight: 500; line-height: 20px; color: var(--muted); } +.figure-row { display: flex; align-items: baseline; gap: 4px; height: 48px; margin-top: 4px; } +.figure { + font-size: var(--fs-figure); line-height: 48px; font-weight: 500; letter-spacing: -0.02em; + color: var(--data); +} +.figure.dash { color: var(--muted); } +.figure-row .unit { font-size: var(--fs-body); } +/* Two lines reserved so a longer finding never pushes the cards down. */ +.finding { margin-top: 6px; min-height: 40px; color: var(--text); } + +.strip > div { padding: 10px 12px; } + +.actions { display: grid; gap: 8px; margin-top: 10px; } + +.progress { height: 28px; display: grid; align-content: space-between; } +.phases { display: flex; justify-content: space-between; color: var(--muted); font-size: var(--fs-small); line-height: 16px; } +.phases li { transition: color 160ms var(--ease); } +.phases li.is-active { color: var(--text); font-weight: 500; } +.phases li.is-done { color: var(--text); } +.progress-track { display: block; height: 3px; border-radius: 2px; background: var(--raised); overflow: hidden; } +.progress-track i { display: block; height: 100%; width: 0; background: var(--data); transform-origin: left; } +.progress:not(.is-running) .phases, .progress:not(.is-running) .progress-track { visibility: hidden; } + +.foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; height: 32px; } +.status { color: var(--muted); font-size: var(--fs-small); min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.status.is-error { color: var(--negative); } +.foot-tools { display: flex; flex: none; } +.foot-tools .btn { height: 32px; } +.foot .menu { right: 0; bottom: calc(100% + 6px); } diff --git a/packages/extension/static/vendor/charts.js b/packages/extension/static/vendor/charts.js new file mode 100644 index 0000000..43fb27f --- /dev/null +++ b/packages/extension/static/vendor/charts.js @@ -0,0 +1,618 @@ +// Vendored from tasteskill (MIT, Copyright (c) 2026 Leonxlnx), https://github.com/Leonxlnx/taste-skill +// tasteskill / charts.js — small, dependency-free charts for app screens, drawn in SVG from your data. +// They follow references/app-screens.md: one question per chart, one series that matters, 3–5 quiet +// horizontal gridlines, round ticks that top out above the largest value, 2px straight lines with no +// markers, square bars, no gradients, a bare tooltip, tabular figures. Colours come from CSS variables: +// --chart-1 … --chart-4 (series), --chart-muted (other series), --chart-mark (a marked period; ink by default), +// --chart-span (the opacity of a shaded span: about .06 on light, .12 on dark), --chart-track (a time strip's track), +// --grid, --axis-ink, --ink, --surface, --raised, --rule, --negative +// Usage (after this file loads): +// Charts.line(el, { labels: ['1 Oct', …], series: [{ name: 'Requests', values: [...] }], format: Charts.fmt.compact }) +// Charts.bars(el, { labels, values, highlight: 6, format }) one bar (or [indices]) may carry the accent; dim: [indices] +// Charts.diverging(el, { labels, up, down, above: 'Sold', below: 'Bought', mark: [3, 9], markLabel: 'Car charged' }) +// Charts.ring(el, { value: 0.72, label: '72%', sub: 'of capacity' }) a progress ring with its total inside +// Charts.barList(el, [{ label: 'Berlin', value: 412 }, …], { format, thin }) a ranked list where the row is the bar +// Charts.plan(el, [{ label: 'Groceries', planned: 600, spent: 412, committed: 60 }, …], { scale: 'row' | 'plan' }) planned against spent +// Charts.bullet(el, [{ label: 'Bonds', value: 0.31, target: 0.25 }, …]) share against a target +// Charts.timeline(el, rows, kinds, { days, today, weekends }) who is away when, each kind of booking its own mark +// Charts.matrix(el, rows, { cols, comfort: 2, today }) slack against a minimum, team by day +// Charts.timeStrip(el, rows, { open: '08:00', close: '15:00', compareLabel: 'Last Sunday' }) when, within the day +// line, bars and diverging take annotations: [{ at: index, label: 'Deploy a3f09c1', short: 'a3f09c1' }] or spans +// [{ from, to, label: 'Cloud' }]. Their labels sit in rows above the plot, measured once the fonts have loaded, and no +// leader line crosses another label; the rows are reserved only when a label exists. reference: { value, label }, its +// label set where no series passes. +// line takes step: true for a balance that only changes on given days; today: index (the forecast after it is dashed) +// with todayLabel: 'Now 20:34' (set clear of the line); endLabel: true (the last value at the line's end); max: caps a +// flat series with one spike (the peak is labelled); yTicks: fixed ticks for a bounded quantity (0–100%); tickFormat: +// axis labels apart from the tooltip format; xAt: the indices to label. Without xAt the x labels keep one step. +// diverging, timeline, matrix and timeStrip return { svg, legend }, and line returns { legend } for two series or more: +// the legend is drawn from the same style objects as the marks, so a swatch is always the mark scaled down. Put it in +// the card's header, or pass legend: element (or a selector) to have it filled. +// Numbers follow the page's language (). Every chart gets role="img" and an aria-label that states what it shows. +(() => { + const NS = 'http://www.w3.org/2000/svg', MINUS = '\u2212', ROW = 14, LANE = (rows) => 8 + rows * ROW + (rows ? 4 : 0); + const css = (el, name, fb) => getComputedStyle(el).getPropertyValue(name).trim() || fb; + const cssNum = (el, name, fb) => { const v = parseFloat(css(el, name, '')); return Number.isFinite(v) ? v : fb; }; + const svg = (tag, attrs = {}, parent) => { const n = document.createElementNS(NS, tag); for (const [k, v] of Object.entries(attrs)) if (v != null) n.setAttribute(k, v); if (parent) parent.appendChild(n); return n; }; + const text = (parent, str, attrs) => { const t = svg('text', attrs, parent); t.textContent = str; return t; }; + const titled = (parent, str) => { svg('title', {}, parent).textContent = str; return parent; }; + // numbers follow the page's language (), so separators match the rest of the screen + const nf = (o) => new Intl.NumberFormat(document.documentElement.lang || 'en', o); + const fmt = { + int: (v) => nf({ maximumFractionDigits: 0 }).format(v), + dec: (d) => (v) => nf({ minimumFractionDigits: d, maximumFractionDigits: d }).format(v), + pct: (d = 1) => (v) => nf({ style: 'percent', minimumFractionDigits: d, maximumFractionDigits: d }).format(v), + money: (cur = 'EUR', d = 0) => (v) => nf({ style: 'currency', currency: cur, minimumFractionDigits: d, maximumFractionDigits: d }).format(v), + compact: (v) => nf({ notation: 'compact', maximumFractionDigits: 1 }).format(v), + ms: (v) => `${nf({ maximumFractionDigits: 0 }).format(v)} ms`, + // a sign on every change, with a true minus: fmt.signed() → '+2', '0', '−1'; fmt.signed(fmt.dec(1)) → '+0.4' + signed: (f = (v) => nf({ maximumFractionDigits: 0 }).format(v)) => (v) => (v > 0 ? '+' : v < 0 ? MINUS : '') + f(Math.abs(v)), + // minutes since midnight as a 24-hour time: 680 → '11:20' + clock: (m) => `${String(Math.floor(m / 60) % 24).padStart(2, '0')}:${String(Math.round(m % 60)).padStart(2, '0')}`, // taste-skill-ignore: a clock, not a counter + }; + const minutes = (t) => (t == null || t === '' ? null : typeof t === 'number' ? t : (([hh, mm]) => hh * 60 + (mm || 0))(String(t).split(':').map(Number))); + // Round ticks (steps of 1, 2, 2.5 or 5 × 10^n) whose top sits above the largest value. + function ticks(min, max, count = 4) { + if (max === min) max = min + 1; + const raw = (max - min) / count, mag = 10 ** Math.floor(Math.log10(raw)); + const step = [1, 2, 2.5, 5, 10].map((m) => m * mag).find((s) => s >= raw) || 10 * mag; + const lo = Math.floor(min / step) * step; let hi = Math.ceil(max / step) * step; if (hi <= max) hi += step; + const out = []; for (let v = lo; v <= hi + step / 2; v += step) out.push(+v.toFixed(10)); + return out; + } + // x labels keep one step all the way: every k-th label, with k chosen so the labels fit the width, and a step that + // lands on the last label when one gives enough labels. Never a forced end label that breaks the step. Clock labels + // ('08:30') step in whole hours (or halves and quarters) from a round time, so a half-hourly axis reads 06:00, 09:00. + function xPicks(labels, span, at) { + const n = labels.length; + if (at) return at.filter((i) => i >= 0 && i < n); + if (n <= 1) return n ? [0] : []; + const longest = Math.max(...labels.map((l) => String(l).length)); + const cap = Math.max(2, Math.min(8, Math.floor(span / (longest * 7 + 18)))); + if (n <= cap) return [...Array(n).keys()]; + const mins = labels.every((l) => /^\d{1,2}:\d{2}$/.test(String(l))) ? labels.map(minutes) : null; + const d = mins ? mins[1] - mins[0] : 0; + let first = null, land = null; + for (let k = 1; k < n; k++) { + const stepM = k * d, clock = mins && d > 0; + if (clock && (stepM >= 60 ? ![1, 2, 3, 4, 6, 12].includes(stepM / 60) : 60 % stepM !== 0)) continue; + // start on a round time: a multiple of the step (09:00 for three hours), or a whole hour for shorter steps + let s = clock ? mins.findIndex((m) => m % Math.max(60, stepM) === 0) : 0; + if (s < 0) s = Math.max(0, mins.findIndex((m) => m % 60 === 0)); + const count = Math.floor((n - 1 - s) / k) + 1; + if (count > cap) continue; + if (first == null) first = { k, s }; + if ((n - 1 - s) % k === 0) { land = { k, s, count }; break; } + } + const pick = land && land.count >= Math.max(3, Math.ceil(cap * 0.6)) ? land : first || { k: n - 1, s: 0 }; + const out = []; for (let i = pick.s; i < n; i += pick.k) out.push(i); + return out; + } + // colours: tokens resolved to rgb, so a mark can be mixed with the surface (a pale fill, a darker step) + const swatchProbe = document.createElement('span'); swatchProbe.style.display = 'none'; + const rgb = (el, value) => { + el.appendChild(swatchProbe); swatchProbe.style.color = ''; swatchProbe.style.color = value; + const c = getComputedStyle(swatchProbe).color; swatchProbe.remove(); + const m = c.match(/-?[\d.]+/g); if (!m || m.length < 3) return null; + return c.startsWith('color(') ? m.slice(0, 3).map((v) => Math.round(+v * 255)) : m.slice(0, 3).map(Number); + }; + const mix = (el, a, b, t) => { const p = rgb(el, a), q = rgb(el, b); return p && q ? `rgb(${p.map((v, i) => Math.round(v * t + q[i] * (1 - t))).join(',')})` : a; }; + // text width, measured in the chart's own face (falls back to an estimate when the chart is hidden) + const measure = (s, str, size = 11, weight = 400) => { const t = text(s, str, { x: -999, y: -999, 'font-size': size, 'font-weight': weight, visibility: 'hidden' }); let wd = 0; try { wd = t.getComputedTextLength(); } catch {} t.remove(); return wd > 0 ? wd : String(str).length * size * 0.56; }; + const fit = (s, str, max, size, weight) => { str = String(str); if (measure(s, str, size, weight) <= max) return str; while (str.length > 1 && measure(s, str + '\u2026', size, weight) > max) str = str.slice(0, -1); return str.trimEnd() + '\u2026'; }; + function frame(el, h) { + el.innerHTML = ''; + const w = Math.max(240, el.clientWidth || 600); + const s = svg('svg', { viewBox: `0 0 ${w} ${h}`, width: '100%', height: h, role: 'img', style: 'display:block;overflow:visible;font-variant-numeric:tabular-nums' }, el); + return { s, w, h }; + } + function tooltip(el) { + let t = el.querySelector('.chart-tip'); + if (!t) { t = document.createElement('div'); t.className = 'chart-tip'; t.style.cssText = 'position:absolute;pointer-events:none;padding:6px 8px;font:inherit;font-size:12px;line-height:1.35;border-radius:6px;white-space:nowrap;opacity:0;transition:opacity 120ms ease;background:var(--surface,#fff);color:var(--ink,#111);border:1px solid var(--grid,#e5e7eb)'; if (getComputedStyle(el).position === 'static') el.style.position = 'relative'; el.appendChild(t); } + return t; + } + function axes(g, el, { w, h, padL, padB, yt, y, labels, x, band = false, at = null, tick, avoid = null }) { + const grid = css(el, '--grid', '#e8e8e8'), ink = css(el, '--axis-ink', '#6b6b6b'), s = g.ownerSVGElement; + for (const v of yt) { + const yy = y(v); + svg('line', { x1: padL, x2: w, y1: yy, y2: yy, stroke: grid, 'stroke-width': 1, 'shape-rendering': 'crispEdges' }, g); + text(g, tick(v), { x: padL - 8, y: yy + 4, 'text-anchor': 'end', 'font-size': 12, fill: ink }); + } + const n = labels.length; + for (const i of xPicks(labels, w - padL, at)) { + const lab = String(labels[i]), tw = measure(s, lab, 12), xx = x(i); + // the first and last labels are pulled inside the plot; none runs past either edge + let left = band ? xx - tw / 2 : i === 0 ? xx : i === n - 1 ? xx - tw : xx - tw / 2; + left = Math.max(band ? 0 : padL, Math.min(w - tw, left)); + if (avoid && left < avoid[1] + 8 && left + tw > avoid[0] - 8) continue; + text(g, lab, { x: left, y: h - padB + 18, 'font-size': 12, fill: ink }); + } + } + // Annotation labels sit in rows above the plot, never on the data. A label sits beside the top of its own leader + // line, right of it or left of it, in one of three rows. A place is free when no other label in that row overlaps + // it, no leader line passes through it, and its own leader passes through no label below it. The layout is searched + // (close markers often need the first label on the left so the next leader runs clear): full labels first, then + // short forms (`short`), and only then is a label dropped to its marker's tooltip, with a note in the console. + function lane(s, items, w, maxRows = 3) { + const gap = 6, rank = (it) => (it.fixed != null ? 0 : it.span ? 1 : 2); + // fixed captions first, then spans, then markers from left to right + const order = [...items].sort((a, b) => rank(a) - rank(b) || (a.x ?? a.x0) - (b.x ?? b.x0)); + const opts = order.map((it) => { + const out = []; + for (const str of [it.label, it.short].filter(Boolean)) { + const tw = measure(s, str, 11) + (it.glyph ? 9 : 0), mid = it.span ? (it.x0 + it.x1) / 2 : 0; + const cands = it.fixed != null ? [[it.fixed, it.fixed + tw]] : it.span ? [[mid - tw / 2, mid + tw / 2], [it.x0, it.x0 + tw], [it.x1 - tw, it.x1]] : [[it.x + 4, it.x + 4 + tw], [it.x - 4 - tw, it.x - 4]]; + for (let row = 0; row < maxRows; row++) for (const [x0, x1] of cands) if (x0 >= 0 && x1 <= w) out.push({ ...it, text: str, isShort: str !== it.label, row, x0, x1 }); + } + return out; + }); + const free = (c, placed) => placed.every((p) => p.dropped || (!(p.row === c.row && c.x0 < p.x1 + gap && c.x1 > p.x0 - gap) + && !(p.leader != null && p.row >= c.row && p.leader > c.x0 - 3 && p.leader < c.x1 + 3) + && !(c.leader != null && p.row <= c.row && c.leader > p.x0 - 3 && c.leader < p.x1 + 3))); + let best = null, steps = 0; + const solve = (i, placed, allowShort, allowDrop) => { + if (++steps > 20000) return false; + if (i === order.length) { best = placed.slice(); return true; } + for (const c of opts[i]) { if ((c.isShort && !allowShort) || !free(c, placed)) continue; placed.push(c); if (solve(i + 1, placed, allowShort, allowDrop)) return true; placed.pop(); } + if (allowDrop) { placed.push({ ...order[i], dropped: true }); if (solve(i + 1, placed, allowShort, allowDrop)) return true; placed.pop(); } + return false; + }; + for (const [sh, dr] of [[false, false], [true, false], [true, true]]) { steps = 0; if (solve(0, [], sh, dr)) break; } + const placed = (best || []).filter((p) => !p.dropped), dropped = (best || order.map((it) => ({ ...it, dropped: true }))).filter((p) => p.dropped); + return { placed, dropped, rows: placed.length ? Math.max(...placed.map((p) => p.row)) + 1 : 0 }; + } + function prepNotes(s, annotations = [], x, w, extra = []) { + const items = []; + for (const a of annotations || []) { + if (a.from != null) items.push({ span: true, x0: Math.min(x(a.from), x(a.to)), x1: Math.max(x(a.from), x(a.to)), label: a.label, short: a.short, colour: a.colour }); + else if (a.at != null) items.push({ x: x(a.at), at: a.at, leader: x(a.at), label: a.label, short: a.short, colour: a.colour }); + } + items.push(...extra); + return { items, ...lane(s, items.filter((it) => it.label), w) }; + } + // shaded spans go under the data; marker lines and the label rows over it + function notesUnder(g, el, N, { padT, h, padB }) { + const ink = css(el, '--axis-ink', '#6b6b6b'), op = cssNum(el, '--chart-span', 0.07); + for (const it of N.items) if (it.span) svg('rect', { x: it.x0, y: padT, width: it.x1 - it.x0, height: h - padT - padB, fill: ink, opacity: op }, g); + } + function notesOver(g, el, N, { padT, h, padB }) { + const ink = css(el, '--axis-ink', '#6b6b6b'); + // a marker line runs to the plot's foot, or stops above the bar it points at (it.end) + for (const it of N.items) if (it.leader != null && !it.drawn) svg('line', { x1: it.leader, x2: it.leader, y1: padT, y2: Math.max(padT, it.end ?? h - padB), stroke: ink, 'stroke-width': 1, opacity: 0.45, 'shape-rendering': 'crispEdges' }, g); + for (const p of N.placed) { + const base = padT - 9 - p.row * ROW; + if (p.leader != null) svg('line', { x1: p.leader, x2: p.leader, y1: base - 8, y2: padT, stroke: ink, 'stroke-width': 1, opacity: 0.45, 'shape-rendering': 'crispEdges' }, g); + if (p.glyph) svg('path', { d: p.glyph === 'up' ? `M${p.x0} ${base - 1}l3 -6l3 6z` : `M${p.x0} ${base - 7}l3 6l3 -6z`, fill: ink }, g); + text(g, p.text, { x: p.x0 + (p.glyph ? 9 : 0), y: base, 'font-size': 11, fill: p.colour || ink }); + } + const el2 = g.ownerSVGElement.parentNode; el2._warned = el2._warned || new Set(); + for (const d of N.dropped) { + if (d.leader != null) { const hit = svg('line', { x1: d.leader, x2: d.leader, y1: padT, y2: h - padB, stroke: 'transparent', 'stroke-width': 8 }, g); titled(hit, d.label, {}); } + if (!el2._warned.has(d.label)) { el2._warned.add(d.label); console.warn(`Charts: no room above the plot for "${d.label}". Give it a short form (short) or label fewer markers.`); } + } + } + // does a label box touch a drawn line? polylines are [[x, y], …]; a box is { x0, x1, y0, y1 } + function hits(b, lines) { + for (const pts of lines) for (let k = 1; k < pts.length; k++) { + const a = pts[k - 1], c = pts[k]; if (!a || !c) continue; + if (a[0] === c[0]) { if (a[0] >= b.x0 - 3 && a[0] <= b.x1 + 3 && Math.max(a[1], c[1]) >= b.y0 - 3 && Math.min(a[1], c[1]) <= b.y1 + 3) return true; continue; } + const lo = Math.max(b.x0 - 3, Math.min(a[0], c[0])), hi = Math.min(b.x1 + 3, Math.max(a[0], c[0])); + for (let xx = lo; xx <= hi; xx += 2) { const yy = a[1] + ((xx - a[0]) / (c[0] - a[0])) * (c[1] - a[1]); if (yy >= b.y0 - 3 && yy <= b.y1 + 3) return true; } + } + return false; + } + const boxHits = (b, boxes) => boxes.some((r) => b.x0 < r.x1 + 3 && b.x1 > r.x0 - 3 && b.y0 < r.y1 + 3 && b.y1 > r.y0 - 3); + // a reference line (a budget, an SLO) with its label at an end where no series passes + function references(g, el, refs, { y, padL, w, lines = [], boxes = [] }) { + const ink = css(el, '--axis-ink', '#6b6b6b'), s = g.ownerSVGElement; + for (const r of [].concat(refs || [])) { + const yy = y(r.value); + svg('line', { x1: padL, x2: w, y1: yy, y2: yy, stroke: ink, 'stroke-width': 1, 'stroke-dasharray': '4 4', opacity: 0.7 }, g); + if (!r.label) continue; + const tw = measure(s, r.label, 11), right = r.at !== 'left'; + const cand = [[right, -5], [right, 13], [!right, -5], [!right, 13]].map(([rt, dy]) => ({ x0: rt ? w - tw : padL + 4, x1: rt ? w : padL + 4 + tw, y0: yy + dy - 9, y1: yy + dy + 2, base: yy + dy })); + const b = cand.find((c) => !hits(c, lines) && !boxHits(c, boxes)) || cand[0]; + text(g, r.label, { x: b.x0, y: b.base, 'font-size': 11, fill: ink }); + } + } + // One style object per kind of mark draws both the mark and its legend swatch, so the two cannot drift apart. + function paintRect(parent, st, x, y, w, h, r = 0) { + let fill = st.fill || 'none'; + if (st.hatch) { + const s = parent.ownerSVGElement || parent, id = 'h' + Math.random().toString(36).slice(2, 8); + const p = svg('pattern', { id, width: 5, height: 5, patternUnits: 'userSpaceOnUse', patternTransform: 'rotate(45)' }, svg('defs', {}, s)); + svg('rect', { width: 5, height: 5, fill: st.fill }, p); svg('line', { x1: 0, y1: 0, x2: 0, y2: 5, stroke: st.hatch, 'stroke-width': 1.2 }, p); + fill = `url(#${id})`; + } + const inset = st.stroke ? (st.width || 1) / 2 : 0; + const n = svg('rect', { x: x + inset, y: y + inset, width: Math.max(0, w - inset * 2), height: Math.max(0, h - inset * 2), rx: r, fill, stroke: st.stroke, 'stroke-width': st.stroke ? st.width || 1 : null, 'stroke-dasharray': st.dash, opacity: st.opacity, 'shape-rendering': r ? null : 'crispEdges' }, parent); + if (st.edge) svg('rect', { x, y, width: 3, height: h, rx: Math.min(r, 1.5), fill: st.edge }, parent); + return n; + } + const legendCss = ':where(.chart-legend){list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:4px 14px;font-size:12px;line-height:16px;color:var(--muted,#5f6368)}:where(.chart-legend li){display:inline-flex;align-items:center;gap:6px;white-space:nowrap}:where(.chart-legend svg){flex:none;overflow:visible}@media (max-width:480px){:where(.chart-legend){display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}}'; + function legendEl(items, target) { + if (!document.getElementById('charts-legend-css')) { const st = document.createElement('style'); st.id = 'charts-legend-css'; st.textContent = legendCss; document.head.appendChild(st); } + const ul = document.createElement('ul'); ul.className = 'chart-legend'; + for (const it of items) { + const li = document.createElement('li'), sw = svg('svg', { width: it.w || 14, height: 12, 'aria-hidden': 'true' }); + it.draw(sw); li.append(sw, document.createTextNode(it.label)); ul.appendChild(li); + } + const t = typeof target === 'string' ? document.querySelector(target) : target; + if (t) t.replaceChildren(ul); + return ul; + } + const rectSwatch = (st, w = 14, h = 10, r = 2) => (sw) => paintRect(sw, st, 0, (12 - h) / 2, w, h, r); + // charts are laid out with measured text: draw again once the page's fonts have arrived + function settle(draw) { if (document.fonts) { document.fonts.ready.then(draw); document.fonts.addEventListener?.('loadingdone', draw, { once: true }); } } + const heightOf = (h) => (typeof h === 'function' ? h() : h); + + function line(el, { labels, series, format = fmt.int, tickFormat = null, height = 220, fill = false, min, max, step = false, annotations, reference, today = null, todayLabel = 'Today', endLabel = false, yTicks = null, xAt = null, legend = null }) { + el._fmt = format; + const colourOf = (sr, k) => sr.colour || (k === 0 ? css(el, '--chart-1', '#2f6fdf') : css(el, `--chart-${k + 1}`, css(el, '--chart-muted', '#a3a3a3'))); + const draw = () => { + const { s, w, h } = frame(el, heightOf(height)); + const all = series.flatMap((x) => x.values.filter((v) => v != null)).concat([].concat(reference || []).map((r) => r.value)); + // max caps the range (a flat series with one spike): values above it are clipped and their peak labelled + const top = max ?? Math.max(...all); + const yt = yTicks || ticks(min ?? Math.min(0, ...all), top, 4); + const tick = tickFormat || format; + const lastVal = series[0].values.filter((v) => v != null).at(-1); + const padL = 12 + Math.max(...yt.map((v) => measure(s, tick(v), 12))), padB = 26, padR = endLabel ? 10 + measure(s, format(lastVal), 12, 600) : 0; + const x = (i) => padL + (i * (w - padL - padR)) / Math.max(1, labels.length - 1); + // the peaks clipped by max are labelled in the rows above the plot, where the line leaves it + const peaks = []; + series.forEach((sr, k) => sr.values.forEach((v, i) => { if (v != null && v > yt.at(-1) && (i === 0 || sr.values[i - 1] == null || sr.values[i - 1] <= yt.at(-1))) { let j = i, peak = v; while (j < sr.values.length && sr.values[j] != null && sr.values[j] > yt.at(-1)) { peak = Math.max(peak, sr.values[j]); j++; } peaks.push({ x: x(i), label: format(peak) + ' at ' + labels[i], colour: colourOf(sr, k) }); } })); + const scale = (N) => { const padT = LANE(N.rows); return { padT, y: (v) => padT + (h - padT - padB) * (1 - (v - yt[0]) / (yt.at(-1) - yt[0])) }; }; + const linesAt = (y) => series.map((sr) => { const out = []; sr.values.forEach((v, i) => { if (v == null) { out.push(null); return; } const p = [x(i), y(Math.min(v, yt.at(-1)))]; const prev = out.at(-1); if (step && prev) out.push([p[0], prev[1]]); out.push(p); }); return out; }).concat([].concat(reference || []).map((r) => [[padL, y(r.value)], [w, y(r.value)]])); + let N = prepNotes(s, annotations, x, w, peaks), { padT, y } = scale(N), todayBox = null; + // the today label goes inside the plot on whichever side of its line no series crosses; failing that, above + if (today != null && todayLabel) { + const cut = x(today), tw = measure(s, todayLabel, 11), lines = linesAt(y); + const cand = [[cut + 4, h - padB - 6], [cut - 4 - tw, h - padB - 6], [cut + 4, padT + 11], [cut - 4 - tw, padT + 11]].map(([x0, b]) => ({ x0, x1: x0 + tw, y0: b - 9, y1: b + 2, base: b })); + todayBox = cand.find((b) => b.x0 >= padL && b.x1 <= w && !hits(b, lines)) || null; + if (!todayBox) { N = prepNotes(s, annotations, x, w, [...peaks, { x: cut, leader: cut, drawn: true, label: todayLabel }]); ({ padT, y } = scale(N)); } + } + const g = svg('g', {}, s); + axes(g, el, { w, h, padL, padB, yt, y, labels, x, at: xAt, tick }); + notesUnder(g, el, N, { padT, h, padB }); + series.forEach((sr, k) => { + const col = colourOf(sr, k); + const pts = sr.values.map((v, i) => (v == null ? null : [x(i), y(Math.min(v, yt.at(-1)))])); + // step: a balance that only changes on given days holds its value until the next one + const d = pts.reduce((acc, p, i) => (p ? acc + (acc && pts[i - 1] ? (step ? `L${p[0].toFixed(1)} ${pts[i - 1][1].toFixed(1)}L` : 'L') : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1) : acc), ''); + if (fill && k === 0) { const first = pts.find(Boolean), last = [...pts].reverse().find(Boolean); svg('path', { d: `${d}L${last[0]} ${y(yt[0])}L${first[0]} ${y(yt[0])}Z`, fill: col, opacity: 0.12 }, g); } + if (today == null) svg('path', { d, fill: 'none', stroke: col, 'stroke-width': 2, 'stroke-linejoin': 'round', 'stroke-linecap': 'round' }, g); + else { + // a forecast: the line after today is dashed, so a projection never reads as a fact + const cut = x(today), id = 'c' + k + Math.random().toString(36).slice(2, 8); + const defs = svg('defs', {}, g); + const a = svg('clipPath', { id: id + 'a' }, defs); svg('rect', { x: 0, y: 0, width: cut, height: h }, a); + const b = svg('clipPath', { id: id + 'b' }, defs); svg('rect', { x: cut, y: 0, width: w, height: h }, b); + svg('path', { d, fill: 'none', stroke: col, 'stroke-width': 2, 'stroke-linejoin': 'round', 'clip-path': 'url(#' + id + 'a)' }, g); + svg('path', { d, fill: 'none', stroke: col, 'stroke-width': 2, 'stroke-dasharray': '5 4', opacity: 0.8, 'clip-path': 'url(#' + id + 'b)' }, g); + if (k === 0) { svg('line', { x1: cut, x2: cut, y1: padT, y2: h - padB, stroke: css(el, '--axis-ink', '#6b6b6b'), 'stroke-width': 1, opacity: 0.4 }, g); if (todayBox) text(g, todayLabel, { x: todayBox.x0, y: todayBox.base, 'font-size': 11, fill: css(el, '--axis-ink', '#6b6b6b') }); } + } + if (endLabel && k === 0) { const li = sr.values.map((v, i) => (v == null ? -1 : i)).filter((i) => i >= 0).at(-1); text(g, format(sr.values[li]), { x: x(li) + 6, y: y(Math.min(sr.values[li], yt.at(-1))) + 4, 'font-size': 12, 'font-weight': 600, fill: css(el, '--ink', '#111') }); } + }); + references(g, el, reference, { y, padL, w, lines: linesAt(y).slice(0, series.length) }); + notesOver(g, el, N, { padT, h, padB }); + // hover: a rule, a dot per series and a bare tooltip + const tip = tooltip(el), rule = svg('line', { y1: padT, y2: h - padB, stroke: css(el, '--axis-ink', '#6b6b6b'), 'stroke-width': 1, opacity: 0 }, g); + const dots = series.map((sr, k) => svg('circle', { r: 3.5, fill: css(el, '--surface', '#fff'), stroke: colourOf(sr, k), 'stroke-width': 2, opacity: 0 }, g)); + const hit = svg('rect', { x: padL, y: padT, width: w - padL, height: h - padT - padB, fill: 'transparent' }, s); + const move = (ev) => { const r = s.getBoundingClientRect(); const px = ((ev.clientX - r.left) / r.width) * w; const i = Math.max(0, Math.min(labels.length - 1, Math.round(((px - padL) / (w - padL - padR)) * (labels.length - 1)))); rule.setAttribute('x1', x(i)); rule.setAttribute('x2', x(i)); rule.setAttribute('opacity', 0.35); series.forEach((sr, k) => { const v = sr.values[i]; dots[k].setAttribute('cx', x(i)); dots[k].setAttribute('cy', v == null ? -99 : y(Math.min(v, yt.at(-1)))); dots[k].setAttribute('opacity', v == null ? 0 : 1); }); tip.innerHTML = `
      ${labels[i]}
      ` + series.map((sr) => `
      ${sr.values[i] == null ? '–' : format(sr.values[i])} ${series.length > 1 ? sr.name : ''}
      `).join(''); const left = Math.min((x(i) / w) * r.width + 12, r.width - tip.offsetWidth - 4); tip.style.left = `${Math.max(0, left)}px`; tip.style.top = '0px'; tip.style.opacity = 1; }; + hit.addEventListener('pointermove', move); hit.addEventListener('pointerleave', () => { tip.style.opacity = 0; rule.setAttribute('opacity', 0); dots.forEach((d) => d.setAttribute('opacity', 0)); }); + const last = series[0].values.filter((v) => v != null).at(-1); + s.setAttribute('aria-label', `${series.map((x) => x.name).join(', ')}, ${labels[0]} to ${labels.at(-1)}; latest ${last == null ? 'no data' : format(last)}`); + }; + draw(); watch(el, draw); settle(draw); + const lg = series.length > 1 ? legendEl(series.map((sr, k) => ({ label: sr.name, w: 16, draw: (sw) => svg('line', { x1: 0, x2: 16, y1: 6, y2: 6, stroke: colourOf(sr, k), 'stroke-width': 2, 'stroke-dasharray': sr.dash ? '4 3' : null }, sw) })), legend) : null; + return { legend: lg }; + } + function bars(el, { labels, values, format = fmt.int, tickFormat = null, height = 200, highlight = null, dim = [], colour, mutedColour, annotations, reference, xAt = null }) { + el._fmt = format; + const lit = (i) => highlight == null || (Array.isArray(highlight) ? highlight.includes(i) : highlight === i); + const draw = () => { + const { s, w, h } = frame(el, heightOf(height)); + const yt = ticks(0, Math.max(...values, ...[].concat(reference || []).map((r) => r.value)), 4), tick = tickFormat || format; + const padL = 12 + Math.max(...yt.map((v) => measure(s, tick(v), 12))), padB = 26; + const band = (w - padL) / values.length, bw = Math.max(2, band * 0.62); + const x = (i) => padL + band * i + band / 2; + const N = prepNotes(s, annotations, x, w), padT = LANE(N.rows); + const y = (v) => padT + (h - padT - padB) * (1 - v / yt.at(-1)); + const g = svg('g', {}, s); + axes(g, el, { w, h, padL, padB, yt, y, labels, x, band: true, at: xAt, tick }); + notesUnder(g, el, N, { padT, h, padB }); + for (const it of N.items) if (it.at != null && values[it.at] != null) it.end = y(values[it.at]) - 4; + const main = colour || css(el, '--chart-1', '#2f6fdf'), muted = mutedColour || css(el, '--chart-muted', '#c9c9c9'); + values.forEach((v, i) => { const r = svg('rect', { x: x(i) - bw / 2, y: y(v), width: bw, height: Math.max(0, y(0) - y(v)), fill: lit(i) ? main : muted, opacity: dim.includes(i) ? 0.45 : null, 'shape-rendering': 'crispEdges' }, g); titled(r, `${labels[i]}: ${format(v)}`, {}); }); + references(g, el, reference, { y, padL, w, boxes: values.map((v, i) => ({ x0: x(i) - bw / 2, x1: x(i) + bw / 2, y0: y(v), y1: y(0) })) }); + notesOver(g, el, N, { padT, h, padB }); + s.setAttribute('aria-label', `${labels.length} bars from ${labels[0]} to ${labels.at(-1)}; highest ${format(Math.max(...values))}`); + }; + draw(); watch(el, draw); settle(draw); + } + // Two directions on one scale: what went out above the zero line and what came in below it (sold and bought, + // income and spend). The axis says which is which, with signed ticks and a caption on each side of zero. + // mark: [indices] draws those periods in a second tone with their own legend entry (the nights the car charged). + function diverging(el, { labels, up, down, values, above = 'Above', below = 'Below', mark = [], markLabel = null, markSide = 'below', partial = null, format = fmt.int, tickFormat = null, height = 200, annotations, reference, xAt = null, legend = null }) { + if (values) { up = values.map((v) => Math.max(0, v || 0)); down = values.map((v) => Math.max(0, -(v || 0))); } + up = up || labels.map(() => 0); down = down || labels.map(() => 0); + const st = { above: { fill: css(el, '--chart-1', '#2f6fdf') }, below: { fill: css(el, '--chart-muted', '#c9c9c9') }, mark: { fill: css(el, '--chart-mark', css(el, '--ink', '#111')) } }; + const marked = (i, side) => mark.includes(i) && (markSide === 'both' || markSide === side); + const tick = (v) => (v > 0 ? '+' : v < 0 ? MINUS : '') + (tickFormat || format)(Math.abs(v)); + const draw = () => { + const { s, w, h } = frame(el, heightOf(height)); + const hiUp = Math.max(0, ...up), hiDown = Math.max(0, ...down); + const yt = ticks(-hiDown, hiUp || 1, 4); const stp = yt[1] - yt[0]; if (hiDown > 0 && yt[0] >= -hiDown) yt.unshift(+(yt[0] - stp).toFixed(10)); + const padL = 12 + Math.max(...yt.map((v) => measure(s, tick(v), 12))), padB = 26; + const band = (w - padL) / labels.length, bw = Math.max(2, band * 0.62); + const x = (i) => padL + band * i + band / 2; + // the caption for the upper side opens the label rows above the plot; the lower one sits under the plot, at left + const N = prepNotes(s, annotations, x, w, [{ fixed: padL, glyph: 'up', label: above }]), padT = LANE(N.rows); + const y = (v) => padT + (h - padT - padB) * (1 - (v - yt[0]) / (yt.at(-1) - yt[0])); + const g = svg('g', {}, s), ink = css(el, '--axis-ink', '#6b6b6b'); + const bwid = measure(s, below, 11) + 9; + axes(g, el, { w, h, padL, padB, yt, y, labels, x, band: true, at: xAt, tick, avoid: [padL, padL + bwid] }); + svg('path', { d: `M${padL} ${h - padB + 11}l3 6l3 -6z`, fill: ink }, g); text(g, below, { x: padL + 9, y: h - padB + 18, 'font-size': 11, fill: ink }); + notesUnder(g, el, N, { padT, h, padB }); + for (const it of N.items) if (it.at != null) it.end = y(up[it.at] || 0) - 4; + labels.forEach((lab, i) => { + const o = partial === i ? 0.5 : null; + if (up[i] > 0) { const r = paintRect(g, { ...(marked(i, 'above') ? st.mark : st.above), opacity: o }, x(i) - bw / 2, y(up[i]), bw, Math.max(0, y(0) - y(up[i]) - 1)); titled(r, `${lab}: ${above} ${format(up[i])}`, {}); } + if (down[i] > 0) { const r = paintRect(g, { ...(marked(i, 'below') ? st.mark : st.below), opacity: o }, x(i) - bw / 2, y(0) + 1, bw, Math.max(0, y(-down[i]) - y(0) - 1)); titled(r, `${lab}: ${below} ${format(down[i])}`, {}); } + }); + svg('line', { x1: padL, x2: w, y1: y(0), y2: y(0), stroke: ink, 'stroke-width': 1, opacity: 0.8, 'shape-rendering': 'crispEdges' }, g); + references(g, el, reference, { y, padL, w, boxes: labels.map((_, i) => ({ x0: x(i) - bw / 2, x1: x(i) + bw / 2, y0: y(up[i]), y1: y(-down[i]) })) }); + notesOver(g, el, N, { padT, h, padB }); + s.setAttribute('aria-label', `${above} above the line and ${below} below it, ${labels[0]} to ${labels.at(-1)}`); + }; + draw(); watch(el, draw); settle(draw); + const items = []; + if (up.some((v) => v > 0)) items.push({ label: above, draw: rectSwatch(st.above) }); + if (down.some((v) => v > 0)) items.push({ label: below, draw: rectSwatch(st.below) }); + if (mark.length && markLabel) items.push({ label: markLabel, draw: rectSwatch(st.mark) }); + return { get svg() { return el.querySelector(':scope > svg'); }, legend: legendEl(items, legend) }; + } + function ring(el, { value, label, sub, size = 132, stroke = 8, colour }) { + el.innerHTML = ''; + const r = (size - stroke) / 2, c = 2 * Math.PI * r, v = Math.max(0, Math.min(1, value)); + const wrap = document.createElement('div'); wrap.style.cssText = `position:relative;width:${size}px;height:${size}px`; + const s = svg('svg', { viewBox: `0 0 ${size} ${size}`, width: size, height: size, role: 'img', 'aria-label': `${label}${sub ? ' ' + sub : ''}` }, wrap); + svg('circle', { cx: size / 2, cy: size / 2, r, fill: 'none', stroke: css(el, '--grid', '#e8e8e8'), 'stroke-width': stroke }, s); + svg('circle', { cx: size / 2, cy: size / 2, r, fill: 'none', stroke: colour || css(el, '--chart-1', '#2f6fdf'), 'stroke-width': stroke, 'stroke-dasharray': `${c * v} ${c}`, transform: `rotate(-90 ${size / 2} ${size / 2})` }, s); + const t = document.createElement('div'); t.style.cssText = 'position:absolute;inset:0;display:grid;place-content:center;text-align:center;font-variant-numeric:tabular-nums'; + t.innerHTML = `
      ${label}
      ${sub ? `
      ${sub}
      ` : ''}`; + wrap.appendChild(t); el.appendChild(wrap); + } + // A ranked list. By default the row's fill is the bar (a one-figure list); thin: true puts a 2px grey bar under each + // label instead, ink for the top item, for a ranked list of money in a narrow card. + function barList(el, rows, { format = fmt.int, colour, thin = false } = {}) { + el.innerHTML = ''; + const max = Math.max(...rows.map((r) => r.value)) || 1; + const list = document.createElement('ol'); list.style.cssText = `list-style:none;margin:0;padding:0;display:grid;gap:${thin ? 10 : 2}px;font-variant-numeric:tabular-nums`; + const valueCol = Math.max(...rows.map((r) => String(format(r.value)).length)) + 1; + rows.forEach((r, k) => { + const li = document.createElement('li'); li.style.cssText = 'display:grid;grid-template-columns:minmax(0,1fr) ' + valueCol + 'ch;align-items:center;column-gap:12px'; + const a = document.createElement('span'); a.textContent = r.label; a.style.cssText = 'position:relative;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis'; + const b = document.createElement('span'); b.textContent = format(r.value); b.style.cssText = 'font-weight:500;text-align:right'; + if (thin) { + const bar = document.createElement('span'); bar.style.cssText = `grid-column:1 / -1;height:2px;margin-top:4px;width:${(r.value / max) * 100}%;background:${k === 0 ? colour || 'var(--ink,#111)' : 'var(--chart-muted,#c9c9c9)'}`; + li.append(a, b, bar); + } else { + const track = document.createElement('span'); track.style.cssText = 'position:relative;padding:10px 12px;border-radius:6px;isolation:isolate;min-width:0'; + const bar = document.createElement('span'); bar.style.cssText = `position:absolute;inset:0 auto 0 0;width:${(r.value / max) * 100}%;background:${colour || 'var(--bar-fill, color-mix(in srgb, var(--chart-1, #2f6fdf) 12%, transparent))'};border-radius:inherit;z-index:-1`; + track.append(bar, a); li.append(track, b); + } + list.appendChild(li); + }); + el.appendChild(list); + } + // Planned against spent: the track is the plan, the fill what is spent, a lighter segment what is + // already committed (bills to come), and an overrun past the track's end in the negative colour. + // One row per category: label and figures on one line, the 8px track under them. + function plan(el, rows, { format = fmt.money('EUR', 0), scale = 'row', note = (r) => r.spent > r.planned ? `${format(r.spent - r.planned)} over` : `${format(r.planned - r.spent - (r.committed || 0))} left` } = {}) { + el.innerHTML = ''; + const list = document.createElement('ul'); list.style.cssText = 'list-style:none;margin:0;padding:0;display:grid;gap:14px;font-variant-numeric:tabular-nums'; + const max = Math.max(...rows.map((r) => Math.max(r.planned, r.spent + (r.committed || 0)))); + for (const r of rows) { + const over = r.spent > r.planned; + const li = document.createElement('li'); + const top = document.createElement('div'); top.style.cssText = 'display:flex;justify-content:space-between;gap:12px;margin-bottom:6px'; + top.innerHTML = `${r.label}${format(r.spent)} of ${format(r.planned)} · ${note(r)}`; + // scale 'row': each track is full width and shows the share used (rows in a grid compare); 'plan': as long as its plan + const room = over ? 100 / (1 + Math.max(0.06, (r.spent - r.planned) / r.planned)) : 100; + const track = document.createElement('div'); track.style.cssText = 'position:relative;height:8px;border-radius:999px;background:var(--rule,#eceeef);overflow:visible;width:' + (scale === 'plan' ? (r.planned / max) * room : room) + '%'; + const pct = (v) => (v / r.planned) * 100; + const spent = Math.min(r.spent, r.planned); + track.innerHTML = `` + + (r.committed ? `` : '') + + (over ? `` : ''); + track.setAttribute('role', 'img'); track.setAttribute('aria-label', `${r.label}: ${format(r.spent)} of ${format(r.planned)}${r.committed ? `, ${format(r.committed)} committed` : ''}${over ? ', over plan' : ''}`); + li.append(top, track); list.appendChild(li); + } + el.appendChild(list); + } + // Share against a target: one row per part, the current share as a bar and a 1px tick at the target, + // so drift reads at a glance (never two stacked bars to compare). + function bullet(el, rows, { format = fmt.pct(1), max = Math.max(...rows.map((r) => Math.max(r.value, r.target))) * 1.15 } = {}) { + el.innerHTML = ''; + const list = document.createElement('ul'); list.style.cssText = 'list-style:none;margin:0;padding:0;display:grid;gap:12px;font-variant-numeric:tabular-nums'; + for (const r of rows) { + const drift = r.value - r.target; + const li = document.createElement('li'); li.style.cssText = 'display:grid;grid-template-columns:minmax(90px,30%) 1fr auto;align-items:center;gap:12px'; + li.innerHTML = `${r.label}${format(r.value)} ${drift >= 0 ? '+' : '−'}${format(Math.abs(drift))} vs ${format(r.target)}`; + li.setAttribute('aria-label', `${r.label}: ${format(r.value)}, target ${format(r.target)}`); + list.appendChild(li); + } + el.appendChild(list); + } + // shared by the row charts (timeline, matrix, time strip): a label column with an optional grey subline. + // A subline needs a 40-44px row pitch; one-line labels sit on 32px. + function rowLabel(g, s, el, r, { y0, pitch, lw }) { + const ink = css(el, '--ink', '#111'), muted = css(el, '--axis-ink', '#6b6b6b'); + if (r.sub) { text(g, fit(s, r.label, lw - 12, 14), { x: 0, y: y0 + pitch / 2 - 3, 'font-size': 14, fill: ink }); text(g, fit(s, r.sub, lw - 12, 12), { x: 0, y: y0 + pitch / 2 + 13, 'font-size': 12, fill: muted }); } + else text(g, fit(s, r.label, lw - 12, 14), { x: 0, y: y0 + pitch / 2 + 5, 'font-size': 14, fill: ink }); + } + const dayText = (d) => (typeof d === 'object' && d ? d.label : String(d)); + // day headers: the full label when every one fits its column, else the short form ('19'), else every k-th short + // label with today always shown and its neighbours left out, so no two headers ever touch + function dayHeads(g, s, el, days, { x0, cw, y, today }) { + const ink = css(el, '--ink', '#111'), muted = css(el, '--axis-ink', '#6b6b6b'); + const full = days.map(dayText), short = days.map((d) => (typeof d === 'object' && d ? d.short || dayText(d) : dayText(d).split(' ').at(-1))); + const wide = (labs) => Math.max(...labs.map((t) => measure(s, t, 12))) + 6; + const labs = wide(full) <= cw ? full : short, k = Math.max(1, Math.ceil(wide(labs) / cw)); + labs.forEach((t, i) => { + if (i !== today && (i % k || (today != null && k > 1 && Math.abs(i - today) < k))) return; + text(g, t, { x: x0 + i * cw + cw / 2, y, 'text-anchor': 'middle', 'font-size': 12, 'font-weight': i === today ? 600 : 400, fill: i === today ? ink : muted }); + }); + } + // The style of each kind of booking: a pale fill, a 1px diagonal hatch, a darker grey step, an outline, or the one + // chart hue for the screen's main data element. tone: 'negative' turns the edge and the label red (it needs a decision). + function kindStyle(el, k = {}) { + const ink = css(el, '--ink', '#111'), surface = css(el, '--surface', '#fff'), neg = css(el, '--negative', '#c4242a'), hue = k.colour || css(el, '--chart-1', '#2f6fdf'); + const tone = k.tone === 'negative' ? { stroke: neg, width: 1.5 } : {}; + const dash = k.dash ? { dash: '3 2' } : {}; + if (k.mark === 'hatch') return { fill: mix(el, ink, surface, 0.04), hatch: mix(el, ink, surface, 0.32), ...tone, ...dash }; + if (k.mark === 'step') return { fill: mix(el, ink, surface, 0.24), ...tone, ...dash }; + if (k.mark === 'outline') return { fill: surface, stroke: mix(el, ink, surface, 0.55), width: 1, ...tone, ...dash }; + if (k.mark === 'hue') return { fill: mix(el, hue, surface, 0.18), edge: hue, ...tone, ...dash }; + return { fill: mix(el, ink, surface, 0.12), ...tone, ...dash }; + } + // Who is away when: people (or rooms, vans) down the left, days across, a bar per booking told apart by its kind. + // rows: [{ label: 'Lena Hoffmann', sub: 'Product design · Berlin', bars: [{ from: 3, to: 7, kind: 'holiday', label, note }] }] + // and { group: 'Brand' } rows for a team subheader + // kinds: { holiday: { label: 'Holiday', mark: 'fill' | 'hatch' | 'step' | 'outline' | 'hue', code: 'H', tone, dash } } + // days: ['Mon 19', …] or [{ label: 'Mon 19', short: '19' }]; weekends: [indices]; today: index; + // holidays: [{ day: index, label: 'All Saints’ Day (Lisbon)' }]; onSelect(bar, row) makes the bars buttons. + // A label that does not fit its bar becomes the kind's code inside it, then goes after the bar's end (never into the + // days before it), and is always in the bar's tooltip. + function timeline(el, rows, kinds = {}, { days = [], weekends = [], today = null, holidays = [], labelWidth = null, rowHeight = null, barHeight = 22, onSelect = null, legend = null, extras = true } = {}) { + const draw = () => { + const pitch = rowHeight || (rows.some((r) => r.sub) ? 44 : 32), head = 30, groupH = 30; + const height = head + rows.reduce((a, r) => a + (r.group ? groupH : pitch), 0) + 2; + const { s, w, h } = frame(el, height); + const lw = labelWidth || Math.round(Math.min(220, Math.max(w < 480 ? 96 : 120, w * 0.2))); + const cw = (w - lw) / Math.max(1, days.length), col = (i) => lw + i * cw; + const ink = css(el, '--ink', '#111'), muted = css(el, '--axis-ink', '#6b6b6b'), rule = css(el, '--rule', '#f0f0f0'), raised = css(el, '--raised', '#f4f5f6'), hue = css(el, '--chart-1', '#2f6fdf'); + const g = svg('g', {}, s); + // weekends one tone step darker, faint day lines, the day header (today in ink) + for (const i of weekends) svg('rect', { x: col(i), y: head, width: cw, height: h - head, fill: raised }, g); + for (let i = 0; i <= days.length; i++) svg('line', { x1: col(i), x2: col(i), y1: head, y2: h, stroke: rule, 'stroke-width': 1, 'shape-rendering': 'crispEdges' }, g); + dayHeads(g, s, el, days, { x0: lw, cw, y: head - 10, today }); + for (const hd of holidays) { const c = svg('circle', { cx: col(hd.day) + cw / 2, cy: head - 3, r: 2, fill: muted }, g); titled(c, hd.label, {}); } + if (today != null) svg('line', { x1: col(today) + cw / 2, x2: col(today) + cw / 2, y1: head, y2: h, stroke: hue, 'stroke-width': 1, 'shape-rendering': 'crispEdges' }, g); + let y0 = head; + for (const r of rows) { + if (r.group) { svg('line', { x1: 0, x2: w, y1: y0 + 0.5, y2: y0 + 0.5, stroke: rule, 'stroke-width': 1 }, g); text(g, r.group, { x: 0, y: y0 + 20, 'font-size': 12, 'font-weight': 500, fill: muted }); y0 += groupH; continue; } + rowLabel(g, s, el, r, { y0, pitch, lw }); + const bs = (r.bars || []).filter((b) => b.to >= 0 && b.from < days.length).sort((a, b) => a.from - b.from); + bs.forEach((b, k) => { + const kind = kinds[b.kind] || {}, st = kindStyle(el, kind); + const x0 = col(Math.max(0, b.from)) + 2, x1 = col(Math.min(days.length - 1, b.to) + 1) - 2, by = y0 + (pitch - barHeight) / 2; + const grp = svg('g', onSelect ? { tabindex: 0, role: 'button', style: 'cursor:pointer' } : {}, g); + paintRect(grp, st, x0, by, x1 - x0, barHeight, 4); + const label = b.label ?? kind.label ?? '', code = b.code ?? kind.code, tw = measure(s, label, 12), labInk = kind.tone === 'negative' ? css(el, '--negative', '#c4242a') : ink; + const next = bs[k + 1] ? col(Math.max(0, bs[k + 1].from)) : w; + // over a hatch the label keeps a halo of the bar's own fill, so the lines never run through the words + const halo = st.hatch ? { stroke: st.fill, 'stroke-width': 4, 'stroke-linejoin': 'round', 'paint-order': 'stroke' } : {}; + if (label && tw + 16 <= x1 - x0) text(grp, label, { x: x0 + 8 + (st.edge ? 2 : 0), y: by + barHeight / 2 + 4, 'font-size': 12, fill: labInk, ...halo }); + else if (code && measure(s, code, 12) + 8 <= x1 - x0) text(grp, code, { x: (x0 + x1) / 2, y: by + barHeight / 2 + 4, 'text-anchor': 'middle', 'font-size': 12, fill: labInk, ...halo }); + else if (label && x1 + 6 + tw <= Math.min(next - 6, w)) text(grp, label, { x: x1 + 6, y: by + barHeight / 2 + 4, 'font-size': 12, fill: kind.tone === 'negative' ? labInk : muted }); + titled(grp, `${r.label}: ${label}${b.note ? ' \u00b7 ' + b.note : ''}`, {}); + if (onSelect) { grp.addEventListener('click', () => onSelect(b, r)); grp.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect(b, r); } }); } + }); + y0 += pitch; + } + s.setAttribute('aria-label', `${rows.filter((r) => !r.group).length} rows across ${days.length} days, ${dayText(days[0])} to ${dayText(days.at(-1))}`); + }; + draw(); watch(el, draw); settle(draw); + const used = new Set(rows.flatMap((r) => (r.bars || []).map((b) => b.kind))); + const items = Object.entries(kinds).filter(([id]) => used.has(id)).map(([, k]) => ({ label: k.label, w: 22, draw: (sw) => { const st = kindStyle(el, k); paintRect(sw, st, 0, 0, 22, 12, 3); } })); + if (extras && today != null) items.push({ label: 'Today', w: 8, draw: (sw) => svg('line', { x1: 4, x2: 4, y1: 0, y2: 12, stroke: css(el, '--chart-1', '#2f6fdf'), 'stroke-width': 1.5 }, sw) }); + return { get svg() { return el.querySelector(':scope > svg'); }, legend: legendEl(items, legend) }; + } + // Is there enough cover? A team-by-day grid of slack against the minimum (+2, +1, 0, −1). Cells with room to spare + // stay plain, and one hue ramps toward the negative colour as the slack runs out; the headcount is in the tooltip. + // rows: [{ label: 'Product design', sub: 'needs 7 of 10', values: [2, 1, 0, -1, null, …], titles: ['8 in', …] }] + // cols: ['Mon 19', …]; comfort: the slack that needs no attention (2); null is a day with no cover to count (a dash) + function matrix(el, rows, { cols = [], comfort = 2, today = null, format = fmt.signed(), labelWidth = null, rowHeight = null, levels = ['Room to spare', 'Tight', 'At the minimum', 'Short'], legend = null } = {}) { + const level = (v) => (v >= comfort ? 0 : v > 0 ? 1 : v === 0 ? 2 : 3); + const styles = () => { const neg = css(el, '--negative', '#c4242a'), surface = css(el, '--surface', '#fff'), ink = css(el, '--ink', '#111'); return [{ fill: surface, ink: css(el, '--axis-ink', '#6b6b6b'), stroke: css(el, '--grid', '#e5e5e5') }, { fill: mix(el, neg, surface, 0.1), ink }, { fill: mix(el, neg, surface, 0.26), ink }, { fill: neg, ink: surface }]; }; + const draw = () => { + const pitch = rowHeight || (rows.some((r) => r.sub) ? 44 : 32), head = 26; + const { s, w, h } = frame(el, head + rows.length * pitch); + const lw = labelWidth || Math.round(Math.min(220, Math.max(w < 480 ? 96 : 120, w * 0.22))); + const cw = (w - lw) / Math.max(1, cols.length), ST = styles(), muted = css(el, '--axis-ink', '#6b6b6b'), ink = css(el, '--ink', '#111'); + const g = svg('g', {}, s); + dayHeads(g, s, el, cols, { x0: lw, cw, y: head - 10, today }); + rows.forEach((r, k) => { + const y0 = head + k * pitch; rowLabel(g, s, el, r, { y0, pitch, lw }); + const ch = Math.min(pitch - 6, 28), cy = y0 + (pitch - ch) / 2; + r.values.forEach((v, i) => { + const cx = lw + i * cw + 1; + if (v == null) { svg('rect', { x: cx + cw / 2 - 4, y: cy + ch / 2 - 1, width: 6, height: 2, fill: muted }, g); return; } + const st = ST[level(v)], cell = paintRect(g, { fill: st.fill }, cx, cy, cw - 2, ch, 3); + text(g, format(v), { x: cx + (cw - 2) / 2, y: cy + ch / 2 + 4, 'text-anchor': 'middle', 'font-size': 12, 'font-weight': level(v) === 3 ? 600 : 400, fill: st.ink }); + titled(cell, `${r.label}, ${dayText(cols[i])}: ${r.titles?.[i] || format(v) + ' against the minimum'}`, {}); + }); + }); + s.setAttribute('aria-label', `Cover against the minimum for ${rows.length} rows across ${cols.length} days`); + }; + draw(); watch(el, draw); settle(draw); + const seen = new Set(rows.flatMap((r) => r.values.filter((v) => v != null).map(level))); + const ST = styles(); + const items = [0, 1, 2, 3].filter((l) => seen.has(l)).map((l) => ({ label: levels[l], w: 14, draw: rectSwatch(l === 0 ? { fill: ST[0].fill, stroke: ST[0].stroke } : { fill: ST[l].fill }, 14, 12, 3) })); + return { get svg() { return el.querySelector(':scope > svg'); }, legend: legendEl(items, legend) }; + } + // When, within the day: one row per item on a shared open–close axis. A 2px grey track, an ink dot at today's time and + // a hollow tick at the comparison day's, so "earlier than last Sunday" is drawn and not only written. + // rows: [{ label: 'White sourdough', sub: 'Headingley', at: '11:20', compare: '12:05', value: '11:20', note: '3 h 40 m before close' }] + // at or compare may be null (it did not sell out); step: minutes between axis labels (60). + function timeStrip(el, rows, { open = '08:00', close = '18:00', step = 60, todayLabel = 'Today', compareLabel = null, labelWidth = null, valueWidth = null, rowHeight = null, legend = null } = {}) { + const o = minutes(open), c = minutes(close); + const st = { today: { r: 4.5 }, compare: { w: 5, h: 12 } }; + const dot = (parent, cx, cy) => svg('circle', { cx, cy, r: st.today.r, fill: css(el, '--ink', '#111') }, parent); + const tick = (parent, cx, cy) => svg('rect', { x: cx - st.compare.w / 2, y: cy - st.compare.h / 2, width: st.compare.w, height: st.compare.h, rx: 2.5, fill: css(el, '--surface', '#fff'), stroke: css(el, '--axis-ink', '#6b6b6b'), 'stroke-width': 1.5 }, parent); + const draw = () => { + const narrow = (el.clientWidth || 600) < 560, axisH = 24; + const pitch = rowHeight || (narrow ? 62 : rows.some((r) => r.sub || r.note) ? 44 : 32); + const { s, w, h } = frame(el, rows.length * pitch + axisH); + const lw = narrow ? 0 : labelWidth || Math.round(Math.min(200, Math.max(110, w * 0.26))); + const vw = narrow ? 0 : valueWidth ?? Math.min(w * 0.32, Math.max(0, ...rows.map((r) => Math.max(r.value ? measure(s, r.value, 13, 500) : 0, r.note ? measure(s, r.note, 12) : 0))) + (rows.some((r) => r.value || r.note) ? 16 : 0)); + const x0 = narrow ? 18 : lw + 6, x1 = narrow ? w - 18 : w - vw - 6, tx = (m) => x0 + ((m - o) / Math.max(1, c - o)) * (x1 - x0); + const ink = css(el, '--ink', '#111'), muted = css(el, '--axis-ink', '#6b6b6b'), rule = css(el, '--rule', '#f0f0f0'); + const track = css(el, '--chart-track', mix(el, ink, css(el, '--surface', '#fff'), 0.16)); + const g = svg('g', {}, s), plotH = rows.length * pitch; + // the axis: one step from the opening, the labels thinned by the same rule as the charts + const marks = []; for (let m = Math.ceil(o / step) * step; m <= c; m += step) marks.push(m); + const picks = xPicks(marks.map(fmt.clock), x1 - x0); + for (const i of picks) { const xx = tx(marks[i]); if (!narrow) svg('line', { x1: xx, x2: xx, y1: 0, y2: plotH, stroke: rule, 'stroke-width': 1, 'shape-rendering': 'crispEdges' }, g); const lab = fmt.clock(marks[i]), tw = measure(s, lab, 12); text(g, lab, { x: Math.max(x0 - 2, Math.min(x1 + 6 - tw, xx - tw / 2)), y: plotH + 17, 'font-size': 12, fill: muted }); } + rows.forEach((r, k) => { + const y0 = k * pitch, cy = narrow ? y0 + pitch - 12 : y0 + pitch / 2; + if (narrow) { + const vwid = r.value ? measure(s, r.value, 13, 500) + 12 : 0; + text(g, fit(s, r.label, w - vwid, 14), { x: 0, y: y0 + 16, 'font-size': 14, fill: ink }); + if (r.value) text(g, r.value, { x: w, y: y0 + 16, 'text-anchor': 'end', 'font-size': 13, 'font-weight': 500, fill: ink }); + const second = [r.sub, r.note].filter(Boolean).join(' \u00b7 '); + if (second) text(g, fit(s, second, w, 12), { x: 0, y: y0 + 33, 'font-size': 12, fill: muted }); + } else rowLabel(g, s, el, r, { y0, pitch, lw }); + svg('line', { x1: x0, x2: x1, y1: cy, y2: cy, stroke: track, 'stroke-width': 2, 'stroke-linecap': 'round' }, g); + const cm = minutes(r.compare), am = minutes(r.at); + if (cm != null) titled(tick(g, tx(cm), cy), `${compareLabel || 'Comparison'}: ${fmt.clock(cm)}`, {}); + if (am != null) titled(dot(g, tx(am), cy), `${todayLabel}: ${fmt.clock(am)}`, {}); + if (narrow) return; + if (r.note) { if (r.value) text(g, r.value, { x: w, y: cy - 3, 'text-anchor': 'end', 'font-size': 13, 'font-weight': 500, fill: ink }); text(g, fit(s, r.note, vw - 8, 12), { x: w, y: r.value ? cy + 13 : cy + 4, 'text-anchor': 'end', 'font-size': 12, fill: muted }); } + else if (r.value) text(g, r.value, { x: w, y: cy + 5, 'text-anchor': 'end', 'font-size': 13, 'font-weight': 500, fill: ink }); + }); + s.setAttribute('aria-label', `${rows.length} rows between ${fmt.clock(o)} and ${fmt.clock(c)}`); + }; + draw(); watch(el, draw); settle(draw); + const items = []; + if (rows.some((r) => minutes(r.at) != null)) items.push({ label: todayLabel, w: 10, draw: (sw) => dot(sw, 5, 6) }); + if (compareLabel && rows.some((r) => minutes(r.compare) != null)) items.push({ label: compareLabel, w: 8, draw: (sw) => tick(sw, 4, 6) }); + return { get svg() { return el.querySelector(':scope > svg'); }, legend: legendEl(items, legend) }; + } + function watch(el, draw) { if (!('ResizeObserver' in window)) return; let w = el.clientWidth; new ResizeObserver(() => { if (Math.abs(el.clientWidth - w) > 4) { w = el.clientWidth; draw(); } }).observe(el); } + window.Charts = { line, bars, diverging, ring, barList, plan, bullet, timeline, matrix, timeStrip, ticks, fmt }; +})(); diff --git a/packages/extension/tsconfig.json b/packages/extension/tsconfig.json index ffa5611..e91badc 100644 --- a/packages/extension/tsconfig.json +++ b/packages/extension/tsconfig.json @@ -5,11 +5,14 @@ "moduleResolution": "bundler", "strict": true, "skipLibCheck": true, - "outDir": "dist", - "rootDir": "src", "declaration": false, "noEmit": true, - "types": ["chrome"] + "types": [ + "chrome" + ], + "resolveJsonModule": true }, - "include": ["src/**/*.ts"] + "include": [ + "src/**/*.ts" + ] } diff --git a/packages/server/package.json b/packages/server/package.json index 33116d3..9f8b062 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -8,7 +8,7 @@ "build": "esbuild src/server.ts --bundle --platform=node --target=node18 --outfile=dist/server.js --packages=external", "dev": "node dist/server.js", "start": "node dist/server.js", - "test": "npx vitest run" + "test": "vitest run" }, "dependencies": { "cors": "^2.8.5", diff --git a/packages/server/src/app.test.ts b/packages/server/src/app.test.ts index d726983..abfbad4 100644 --- a/packages/server/src/app.test.ts +++ b/packages/server/src/app.test.ts @@ -1,45 +1,57 @@ import { describe, it, expect } from 'vitest'; import request from 'supertest'; -import app from './app.js'; +import { VERSION } from '@meterx/shared'; +import app from './app'; -describe('GET /ping', () => { - it('returns 200 with Pong', async () => { +const EXT = 'chrome-extension://abcdefghijklmnop'; + +describe('GET/HEAD /ping', () => { + it('GET returns Pong', async () => { const res = await request(app).get('/ping'); expect(res.status).toBe(200); expect(res.text).toBe('Pong!'); }); -}); -describe('HEAD /ping', () => { - it('returns 200', async () => { - const res = await request(app).head('/ping'); - expect(res.status).toBe(200); + it('HEAD returns 200', async () => { + expect((await request(app).head('/ping')).status).toBe(200); }); }); describe('GET /health', () => { - it('returns ok status', async () => { + it('matches the shared HealthResponse shape', async () => { const res = await request(app).get('/health'); expect(res.status).toBe(200); - expect(res.body).toEqual({ status: 'ok', version: '2026.0.319-MR1.0' }); + expect(res.body).toMatchObject({ status: 'ok', server: 'express', version: VERSION }); + expect(typeof res.body.timestamp).toBe('number'); }); }); describe('GET /test-file/:filename', () => { - it('serves 1MB.bin', async () => { - const res = await request(app).get('/test-file/1MB.bin'); + it('streams 1MB.bin', async () => { + const res = await request(app).get('/test-file/1MB.bin').buffer(true).parse((r, cb) => { + let n = 0; r.on('data', (c: Buffer) => { n += c.length; }); r.on('end', () => cb(null, n)); + }); expect(res.status).toBe(200); expect(res.headers['content-type']).toMatch(/octet-stream/); + expect(res.body).toBe(1024 * 1024); + }); + + it.each(['foo.txt', '99MB.bin', '025MB.bin'])('rejects %s with 400', async (f) => { + expect((await request(app).get(`/test-file/${f}`)).status).toBe(400); }); +}); - it('rejects invalid filename', async () => { - const res = await request(app).get('/test-file/foo.txt'); - expect(res.status).toBe(400); +describe('CORS', () => { + it('allows extension origins and sends Timing-Allow-Origin', async () => { + const res = await request(app).get('/ping').set('Origin', EXT); + expect(res.headers['access-control-allow-origin']).toBe(EXT); + expect(res.headers['timing-allow-origin']).toBe(EXT); }); - it('rejects disallowed size', async () => { - const res = await request(app).get('/test-file/99MB.bin'); - expect(res.status).toBe(400); + it.each(['https://evil.example', 'null'])('blocks %s without a 500', async (origin) => { + const res = await request(app).get('/ping').set('Origin', origin); + expect(res.status).toBe(200); + expect(res.headers['access-control-allow-origin']).toBeUndefined(); }); }); @@ -47,30 +59,18 @@ describe('GET /ip', () => { it('returns connecting IP and family', async () => { const res = await request(app).get('/ip'); expect(res.status).toBe(200); - expect(res.body).toHaveProperty('family'); expect(['ipv4', 'ipv6', 'unknown']).toContain(res.body.family); }); - - it('sets Timing-Allow-Origin', async () => { - const res = await request(app).get('/ip'); - expect(res.headers['timing-allow-origin']).toBe('*'); - }); }); describe('POST /upload', () => { it('accepts octet-stream upload', async () => { - const res = await request(app) - .post('/upload') - .set('Content-Type', 'application/octet-stream') - .send(Buffer.alloc(1024)); + const res = await request(app).post('/upload').set('Content-Type', 'application/octet-stream').send(Buffer.alloc(1024)); expect(res.status).toBe(200); }); it('rejects wrong content type', async () => { - const res = await request(app) - .post('/upload') - .set('Content-Type', 'text/plain') - .send('hello'); + const res = await request(app).post('/upload').set('Content-Type', 'text/plain').send('hello'); expect(res.status).toBe(415); }); }); diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index 11b6cac..11ba309 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -1,99 +1,81 @@ /** - * MeterX Speed Test Server + * MeterX self-host speed test server (same API contract as the Worker). * @author ajithakdev (https://github.com/ajithakdev) * @license AGPL-3.0 - * @version 2026.0.319-MR1.0 */ -import express, { Request, Response } from 'express'; +import express, { Request, Response, NextFunction } from 'express'; import cors from 'cors'; import rateLimit from 'express-rate-limit'; -import fs from 'fs'; -import path from 'path'; +import { Readable } from 'stream'; +import { + VERSION, TEST_FILE_RE, MAX_UPLOAD_BYTES, isExtensionOrigin, ipFamily, + type HealthResponse, type IpInfo, +} from '@meterx/shared'; const app = express(); -// Trust proxy (required behind Render/Cloudflare/nginx reverse proxies) +// Trust one proxy hop (Render, Cloudflare, nginx) so req.ip is the client. app.set('trust proxy', 1); -// --- CORS --- -const ALLOWED_ORIGINS = process.env.CORS_ORIGINS - ? process.env.CORS_ORIGINS.split(',').map(o => o.trim()) - : ['http://localhost:3000']; +const ALLOWED_ORIGINS = (process.env.CORS_ORIGINS || 'http://localhost:3000').split(',').map(o => o.trim()); -app.use(cors({ - origin(origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) { - if (!origin) return callback(null, true); - if (origin.startsWith('chrome-extension://') || origin.startsWith('moz-extension://')) { - return callback(null, true); - } - if (ALLOWED_ORIGINS.includes(origin)) return callback(null, true); - callback(new Error('Not allowed by CORS')); - } -})); - -// --- Rate limiting --- -// Higher buckets to allow multi-stream duration-based tests (4 parallel x ~8s window -// can emit 8-16 requests per test). Ping probes the loaded-ping probe needs room too. -const pingLimiter = rateLimit({ windowMs: 60000, max: 240, standardHeaders: true, legacyHeaders: false, message: 'Too many ping requests.' }); -const downloadLimiter = rateLimit({ windowMs: 60000, max: 120, standardHeaders: true, legacyHeaders: false, message: 'Too many download requests.' }); -const uploadLimiter = rateLimit({ windowMs: 60000, max: 120, standardHeaders: true, legacyHeaders: false, message: 'Too many upload requests.' }); - -app.use(express.raw({ type: 'application/octet-stream', limit: '10mb' })); +// Disallowed origins get no CORS headers (the browser blocks them), not a 500 (#81). +app.use(cors({ origin: (origin, cb) => cb(null, !origin || isExtensionOrigin(origin) || ALLOWED_ORIGINS.includes(origin)) })); -// --- Test files --- -const ALLOWED_FILE_SIZES = [1, 5, 10, 25]; -const testFileDir = path.join(__dirname, '..', 'test-files'); -if (!fs.existsSync(testFileDir)) fs.mkdirSync(testFileDir, { recursive: true }); - -function ensureTestFile(sizeMB: number): void { - const filePath = path.join(testFileDir, `${sizeMB}MB.bin`); - if (!fs.existsSync(filePath)) { - try { - fs.writeFileSync(filePath, Buffer.alloc(sizeMB * 1024 * 1024)); - } catch (e) { - console.error(`File generation failed for ${sizeMB}MB:`, e); - } - } -} +// Lets the extension read DNS timing on every route (#82). +app.use((req: Request, res: Response, next: NextFunction) => { + const origin = req.headers.origin; + if (origin && res.getHeader('Access-Control-Allow-Origin') === origin) res.setHeader('Timing-Allow-Origin', origin); + res.setHeader('Cache-Control', 'no-store'); + next(); +}); -// Pre-generate default -ensureTestFile(parseInt(process.env.DOWNLOAD_FILE_MB || '1', 10)); +// Sized for one test (about 150 requests) plus a retry within the minute. +const limiter = (max: number) => rateLimit({ windowMs: 60_000, limit: max, standardHeaders: true, legacyHeaders: false, message: 'Too many requests.' }); +const pingLimiter = limiter(240); +const downloadLimiter = limiter(120); +const uploadLimiter = limiter(600); -// --- Endpoints --- +const CHUNK = Buffer.alloc(64 * 1024); app.get('/test-file/:filename', downloadLimiter, (req: Request, res: Response) => { - const filename = req.params.filename as string; - const match = filename.match(/^(\d+)MB\.bin$/); - if (!match) { res.status(400).send('Invalid file name. Use format: {size}MB.bin'); return; } - const sizeMB = parseInt(match[1], 10); - if (!ALLOWED_FILE_SIZES.includes(sizeMB)) { res.status(400).send(`Invalid size. Allowed: ${ALLOWED_FILE_SIZES.join(', ')}MB`); return; } - - const filePath = path.join(testFileDir, String(filename)); - if (!fs.existsSync(filePath)) ensureTestFile(sizeMB); - if (fs.existsSync(filePath)) { res.sendFile(filePath); } - else { res.status(500).send('Failed to generate test file.'); } + const name = String(req.params.filename); + if (!TEST_FILE_RE.test(name)) { res.status(400).send('Invalid file. Use 1MB.bin, 5MB.bin, 10MB.bin or 25MB.bin.'); return; } + let left = parseInt(name, 10) * 1024 * 1024; + res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader('Content-Length', String(left)); + Readable.from((function* () { + while (left > 0) { const n = Math.min(CHUNK.length, left); left -= n; yield n === CHUNK.length ? CHUNK : CHUNK.subarray(0, n); } + })()).pipe(res); }); +// Drain the body without buffering it (#85). app.post('/upload', uploadLimiter, (req: Request, res: Response) => { - const contentType = req.headers['content-type']; - if (!contentType || !contentType.includes('application/octet-stream')) { + if (!(req.headers['content-type'] || '').includes('application/octet-stream')) { res.status(415).send('Unsupported Media Type. Expected application/octet-stream.'); return; } - res.status(200).send('Upload received.'); + let received = 0; + req.on('data', (chunk: Buffer) => { + received += chunk.length; + if (received > MAX_UPLOAD_BYTES && !res.headersSent) { res.status(413).send('Payload too large'); req.destroy(); } + }); + req.on('end', () => { if (!res.headersSent) res.status(200).send('Upload received.'); }); }); app.head('/ping', pingLimiter, (_req: Request, res: Response) => { res.status(200).end(); }); app.get('/ping', pingLimiter, (_req: Request, res: Response) => { res.status(200).send('Pong!'); }); -app.get('/health', (_req: Request, res: Response) => { res.status(200).json({ status: 'ok', version: '2026.0.319-MR1.0' }); }); + +app.get('/health', (_req: Request, res: Response) => { + const body: HealthResponse = { status: 'ok', server: 'express', version: VERSION, timestamp: Date.now() }; + res.json(body); +}); app.get('/ip', pingLimiter, (req: Request, res: Response) => { const ip = String(req.ip || ''); - const family: 'ipv4' | 'ipv6' | 'unknown' = ip.includes(':') ? 'ipv6' : ip.match(/^\d+\.\d+\.\d+\.\d+$/) ? 'ipv4' : 'unknown'; - res.setHeader('Cache-Control', 'no-store'); - res.setHeader('Timing-Allow-Origin', '*'); - res.status(200).json({ ip, family }); + const info: IpInfo = { ip, family: ipFamily(ip) }; + res.json(info); }); export default app; diff --git a/packages/server/src/test.ts b/packages/server/src/test.ts deleted file mode 100644 index 71ba157..0000000 --- a/packages/server/src/test.ts +++ /dev/null @@ -1,89 +0,0 @@ -const http = require('http'); -const assert = require('assert'); - -// Configuration -const SERVER_URL = 'http://localhost:3000'; -const TIMEOUT = 5000; // 5 seconds - -// Helper function to make HTTP requests -function makeRequest(url, method = 'GET') { - return new Promise((resolve, reject) => { - const req = http.request(url, { method }, (res) => { - let data = ''; - res.on('data', (chunk) => { - data += chunk; - }); - res.on('end', () => { - resolve({ - statusCode: res.statusCode, - headers: res.headers, - data - }); - }); - }); - - req.on('error', (error) => { - reject(error); - }); - - req.setTimeout(TIMEOUT, () => { - req.abort(); - reject(new Error(`Request to ${url} timed out after ${TIMEOUT}ms`)); - }); - - req.end(); - }); -} - -// Test cases -async function runTests() { - console.log('Starting MeterX server tests...'); - - try { - // Test 1: Ping endpoint - console.log('\nTest 1: Testing ping endpoint...'); - const pingResponse = await makeRequest(`${SERVER_URL}/ping`); - assert.strictEqual(pingResponse.statusCode, 200, 'Ping endpoint should return 200 status code'); - console.log('✅ Ping endpoint test passed!'); - - // Test 2: Download file endpoint - console.log('\nTest 2: Testing download file endpoint...'); - const downloadResponse = await makeRequest(`${SERVER_URL}/test-file/1MB.bin`); - assert.strictEqual(downloadResponse.statusCode, 200, 'Download endpoint should return 200 status code'); - assert(downloadResponse.headers['content-length'] > 0, 'Download file should have content'); - console.log('✅ Download endpoint test passed!'); - - // Test 3: Upload endpoint - console.log('\nTest 3: Testing upload endpoint...'); - const uploadResponse = await makeRequest(`${SERVER_URL}/upload`, 'POST'); - assert.strictEqual(uploadResponse.statusCode, 200, 'Upload endpoint should return 200 status code'); - console.log('✅ Upload endpoint test passed!'); - - console.log('\n🎉 All tests passed successfully!'); - } catch (error) { - console.error('\n❌ Test failed:', error.message); - process.exit(1); - } -} - -// Check if server is running before starting tests -async function checkServerRunning() { - try { - await makeRequest(`${SERVER_URL}/ping`); - return true; - } catch { - console.error(`❌ Server is not running at ${SERVER_URL}`); - console.error('Please start the server with "node server.js" before running tests'); - return false; - } -} - -// Main execution -(async () => { - const isServerRunning = await checkServerRunning(); - if (isServerRunning) { - await runTests(); - } else { - process.exit(1); - } -})(); \ No newline at end of file diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index f9933f7..af2b3f1 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -1,16 +1,16 @@ { "compilerOptions": { "target": "ES2022", - "module": "commonjs", - "moduleResolution": "node", + "module": "ES2022", + "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "outDir": "dist", - "rootDir": "src", "declaration": false, - "noEmit": true + "noEmit": true, + "resolveJsonModule": true }, - "include": ["src/**/*.ts"], - "exclude": ["src/test.ts"] + "include": [ + "src/**/*.ts" + ] } diff --git a/packages/shared/package.json b/packages/shared/package.json index d9be1ff..12097d1 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@meterx/shared", - "version": "1.0.0", + "version": "2027.0.0", "private": true, "license": "AGPL-3.0", "type": "module", @@ -8,5 +8,8 @@ "types": "./src/index.ts", "exports": { ".": "./src/index.ts" + }, + "scripts": { + "test": "vitest run" } } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index cee01a9..b0b0aae 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,19 +1,63 @@ /** - * MeterX Shared Types + * MeterX shared contract: types, API constants and helpers used by the + * extension, the Cloudflare Worker and the Express server. * @license AGPL-3.0 */ +import pkg from '../../../package.json'; + +/** Single source of truth for the version, read from the root package.json. */ +export const VERSION: string = pkg.version; + +// --- Server API contract --- + +export const ALLOWED_SIZES_MB = [1, 5, 10, 25] as const; +export const TEST_FILE_RE = /^(1|5|10|25)MB\.bin$/; +/** Bodies above this are cut off by both backends; the extension never sends more than 8 MB. */ +export const MAX_UPLOAD_BYTES = 16 * 1024 * 1024; + +/** Browser extension origins (Chrome, Edge, Opera, Firefox, Safari). Never `null`. */ +export function isExtensionOrigin(origin: string | null | undefined): boolean { + return !!origin && /^(chrome|moz|safari-web)-extension:\/\/[\w.-]+$/.test(origin); +} + +export function ipFamily(ip: string): 'ipv4' | 'ipv6' | 'unknown' { + if (ip.includes(':')) return ip.startsWith('::ffff:') && /\d+\.\d+\.\d+\.\d+$/.test(ip) ? 'ipv4' : 'ipv6'; + return /^\d+\.\d+\.\d+\.\d+$/.test(ip) ? 'ipv4' : 'unknown'; +} + +export interface HealthResponse { + status: 'ok'; + server: 'cloudflare-worker' | 'express'; + version: string; + edge?: string; + timestamp: number; +} + +/** `GET /ip`: connection facts the server can see. Nothing is stored. */ +export interface IpInfo { + ip: string; + family: 'ipv4' | 'ipv6' | 'unknown'; + isp?: string; + edge?: string; + city?: string; + country?: string; +} + +// --- Measurement results --- + export type Quality = 'excellent' | 'good' | 'fair' | 'poor'; +export type Grade = 'A' | 'B' | 'C' | 'D' | 'F'; +export type Phase = 'connect' | 'download' | 'upload' | 'latency' | 'done'; export interface TestProgress { + phase?: Phase; status?: string; downloadSpeed?: number; uploadSpeed?: number; ping?: number; jitter?: number; packetLoss?: number; - bufferbloatDelta?: number; - dnsLookupMs?: number; } export interface TestResults { @@ -22,47 +66,21 @@ export interface TestResults { ping?: number; jitter?: number; packetLoss?: number; + /** Loaded latency minus idle latency, worst of download and upload. */ bufferbloatDelta?: number; - bufferbloatGrade?: 'A' | 'B' | 'C' | 'D' | 'F'; + bufferbloatGrade?: Grade; + loadedPingDown?: number; + loadedPingUp?: number; dnsLookupMs?: number; ipv4Ok?: boolean; ipv6Ok?: boolean; - status: string; - serverUrl?: string; edge?: string; + isp?: string; + serverUrl?: string; timestamp?: number; } -export interface PingResult { - ping: number; - jitter: number; - packetLoss: number; - samples?: number[]; -} - -export interface HealthResponse { - status: 'ok' | 'degraded'; - server: 'cloudflare-worker' | 'express'; - version?: string; - edge?: string; - timestamp: number; -} - -export interface HistoryEntry { - id?: string; - timestamp: number; - downloadSpeed?: number; - uploadSpeed?: number; - ping?: number; - jitter?: number; - packetLoss?: number; - bufferbloatDelta?: number; - bufferbloatGrade?: 'A' | 'B' | 'C' | 'D' | 'F'; - dnsLookupMs?: number; - ipv4Ok?: boolean; - ipv6Ok?: boolean; - edge?: string; -} +export type HistoryEntry = Omit & { timestamp: number }; export interface IspPlan { downloadMbps: number; @@ -70,15 +88,10 @@ export interface IspPlan { providerName?: string; } -export const QUALITY_THRESHOLDS = { - excellent: { download: 100, upload: 20, ping: 30, jitter: 10, packetLoss: 0.5 }, - good: { download: 25, upload: 5, ping: 60, jitter: 20, packetLoss: 1 }, - fair: { download: 5, upload: 1, ping: 120, jitter: 40, packetLoss: 3 }, -} as const; - -export const USE_CASE_REQUIREMENTS = { - calls: { minDownload: 1, minUpload: 1, maxPing: 150, maxJitter: 30, maxPacketLoss: 1 }, - streaming: { minDownload: 5, minUpload: 0, maxPing: 200, maxJitter: 60, maxPacketLoss: 2 }, - gaming: { minDownload: 3, minUpload: 1, maxPing: 50, maxJitter: 15, maxPacketLoss: 0.5 }, - uploads: { minDownload: 0, minUpload: 5, maxPing: 200, maxJitter: 60, maxPacketLoss: 2 }, -} as const; +/** Messages the service worker broadcasts to extension pages. */ +export type BroadcastMessage = + | { action: 'testProgress'; data: TestProgress } + | { action: 'testComplete'; data: HistoryEntry } + | { action: 'testError'; error: string; cancelled?: boolean }; + +export { SHARE_VIEWER, buildShareUrl, parseShareUrl } from './share'; diff --git a/packages/extension/src/share.test.ts b/packages/shared/src/share.test.ts similarity index 56% rename from packages/extension/src/share.test.ts rename to packages/shared/src/share.test.ts index b77290c..6f4de47 100644 --- a/packages/extension/src/share.test.ts +++ b/packages/shared/src/share.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { buildShareUrl, parseShareUrl } from './share'; +import { buildShareUrl, parseShareUrl, SHARE_VIEWER } from './share'; describe('share URL round-trip', () => { it('encodes and decodes a full result', () => { @@ -12,6 +12,7 @@ describe('share URL round-trip', () => { packetLoss: 0, bufferbloatGrade: 'B' as const, edge: 'BOM', + isp: 'Reliance Jio', }; const url = buildShareUrl(entry); expect(url).toContain('#r='); @@ -21,11 +22,20 @@ describe('share URL round-trip', () => { expect(decoded!.downloadSpeed).toBeCloseTo(123.5, 1); expect(decoded!.bufferbloatGrade).toBe('B'); expect(decoded!.edge).toBe('BOM'); + expect(decoded!.isp).toBe('Reliance Jio'); + expect(url.startsWith(SHARE_VIEWER)).toBe(true); + }); + + it('drops fields that fail validation', () => { + const evil = { t: 1, dl: 'x', ul: -5, p: 1e9, bb: 'Z', e: ' diff --git a/packages/extension/static/styles/app.css b/packages/extension/static/styles/app.css index 0442c55..3b5c2f2 100644 --- a/packages/extension/static/styles/app.css +++ b/packages/extension/static/styles/app.css @@ -180,7 +180,12 @@ button, input, select { font: inherit; color: inherit; } .strip dt { color: var(--muted); font-size: var(--fs-small); line-height: 16px; white-space: nowrap; } .strip dd { font-size: 20px; line-height: 28px; font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; } .unit { color: var(--muted); font-size: var(--fs-small); font-weight: 400; margin-left: 2px; } -.dash { color: var(--muted); } +/* No value yet: a short muted bar sized to the figure instead of a dash character; the unit keeps its space but hides. */ +.dash { display: inline-block; position: relative; width: 1.1em; } +/* Hide the hyphen itself; listed with the figure classes so their colours don't win. */ +.dash, .figure.dash, .strip .lead.dash { color: transparent; } +.dash::after { content: ""; position: absolute; left: 0; right: 0.15em; top: 50%; height: max(2px, 0.1em); margin-top: -0.05em; border-radius: 1px; background: var(--control); } +.dash + .unit { visibility: hidden; } /* Latency under load: idle vs loaded, the bufferbloat evidence. */ .load-rows { display: grid; gap: 8px; } diff --git a/packages/extension/static/styles/panel.css b/packages/extension/static/styles/panel.css index b93d7a3..e96654b 100644 --- a/packages/extension/static/styles/panel.css +++ b/packages/extension/static/styles/panel.css @@ -21,7 +21,6 @@ .strip { grid-auto-flow: row; grid-template-columns: repeat(4, 1fr); } .strip dd { font-size: 20px; } .strip .lead { font-size: 28px; line-height: 32px; color: var(--data); letter-spacing: -0.01em; } -.strip .lead.dash { color: var(--muted); } .strip dd { display: flex; align-items: baseline; min-height: 32px; } @media (max-width: 600px) { .strip { grid-template-columns: 1fr 1fr; } @@ -31,23 +30,23 @@ .legend { display: flex; gap: 12px; color: var(--muted); font-size: var(--fs-small); } .legend > * { display: inline-flex; align-items: center; gap: 6px; } -.chart { min-height: 140px; } +.chart { min-height: 96px; } .empty { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; min-height: 40px; padding: 8px 12px; border: 1px dashed var(--control); border-radius: var(--radius-card); color: var(--muted); } -#chartEmpty { min-height: 140px; justify-content: center; text-align: center; } +#chartEmpty { min-height: 96px; justify-content: center; text-align: center; } .link { color: var(--text); text-decoration: underline; } -.plan-rows { display: grid; gap: 12px; } +.plan-rows { display: grid; gap: 10px; } .plan-row .top { display: flex; justify-content: space-between; gap: 8px; } .plan-row .top b { font-weight: 500; font-variant-numeric: tabular-nums; } .plan-row .bar-track { position: relative; height: 8px; margin-top: 6px; border-radius: 4px; background: var(--raised); } .plan-row .bar-track i { position: absolute; inset: 0 auto 0 0; border-radius: 4px; background: var(--text); } .plan-row.down .bar-track i { background: var(--data); } .plan-row .bar-track .target { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--text); } -.plan-row .sub { margin-top: 4px; color: var(--muted); font-size: var(--fs-small); } -.plan-row .sub.is-low { color: var(--negative); } +.plan-row .share { margin-left: 6px; } +.plan-row .share.is-low { color: var(--negative); } .results { width: 100%; border-collapse: collapse; } .results th { text-align: left; color: var(--muted); font-size: var(--fs-small); font-weight: 400; padding: 0 0 6px; } @@ -59,7 +58,8 @@ .results td.when { width: 100%; } .results td.when { color: var(--muted); } .results td.down { color: var(--data); font-weight: 500; } -.status { min-height: 20px; color: var(--muted); font-size: var(--fs-small); } +/* Status sits in the Results header, between the title and Export, so it never needs its own row. */ +.card-head .status { flex: 1; min-width: 0; text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--muted); font-size: var(--fs-small); } .status.is-error { color: var(--negative); } .card-head .menu { right: 0; top: calc(100% + 4px); } @@ -71,10 +71,10 @@ } /* Fit the window: everything stays in place and only the results list scrolls, under pinned column names. - Short windows (under 720px) fall back to scrolling the whole page. */ -@media (min-height: 720px) { + Short windows (under 740px) fall back to scrolling the whole page. */ +@media (min-height: 740px) { .panel .wrap { height: 100vh; display: flex; flex-direction: column; padding-bottom: 12px; } - .panel main { flex: 1; min-height: 0; gap: 10px; grid-template-rows: auto auto auto auto minmax(150px, 1fr) auto; } + .panel main { flex: 1; min-height: 0; gap: 10px; grid-template-rows: auto auto auto auto minmax(132px, 1fr); } .results-card { display: flex; flex-direction: column; min-height: 0; } .results-body { flex: 1; min-height: 0; overflow-y: auto; padding-top: 0; margin-top: 10px; } .results thead th { position: sticky; top: 0; z-index: 1; background: var(--surface); } @@ -93,3 +93,12 @@ .live-track i { display: block; height: 100%; width: 0; background: var(--data); } .panel.is-running .live-track { visibility: visible; } .panel.is-running .strip { border-color: var(--data); } + +/* Short panels: tighter cards so the results list keeps a few rows in view. */ +@media (max-height: 900px) { + .panel main { gap: 8px; } + .panel .card-head { padding-top: 10px; } + .panel .card-body { padding: 8px var(--pad) 10px; } + .panel .strip .lead { font-size: 24px; line-height: 28px; } + .panel .strip dd { min-height: 28px; } +} diff --git a/packages/extension/static/styles/popup.css b/packages/extension/static/styles/popup.css index 187b7ff..a35e01a 100644 --- a/packages/extension/static/styles/popup.css +++ b/packages/extension/static/styles/popup.css @@ -14,7 +14,6 @@ body.popup { font-size: var(--fs-figure); line-height: 48px; font-weight: 500; letter-spacing: -0.02em; color: var(--data); } -.figure.dash { color: var(--muted); } .figure-row .unit { font-size: var(--fs-body); } /* Two lines reserved so a longer finding never pushes the cards down. */ .finding { margin-top: 6px; min-height: 40px; color: var(--text); } From 68b3b5898cf5b9b42ace2c09bf9ef9412da4373c Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Sat, 24 Oct 2026 02:25:26 +0530 Subject: [PATCH 10/44] fix: lint-clean placeholder bar height --- packages/extension/static/styles/app.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension/static/styles/app.css b/packages/extension/static/styles/app.css index 3b5c2f2..d281ce3 100644 --- a/packages/extension/static/styles/app.css +++ b/packages/extension/static/styles/app.css @@ -184,7 +184,7 @@ button, input, select { font: inherit; color: inherit; } .dash { display: inline-block; position: relative; width: 1.1em; } /* Hide the hyphen itself; listed with the figure classes so their colours don't win. */ .dash, .figure.dash, .strip .lead.dash { color: transparent; } -.dash::after { content: ""; position: absolute; left: 0; right: 0.15em; top: 50%; height: max(2px, 0.1em); margin-top: -0.05em; border-radius: 1px; background: var(--control); } +.dash::after { content: ""; position: absolute; left: 0; right: 0.15em; top: 50%; height: 0.1em; min-height: 2px; margin-top: -0.05em; border-radius: 1px; background: var(--control); } .dash + .unit { visibility: hidden; } /* Latency under load: idle vs loaded, the bufferbloat evidence. */ From 755b06d2bf43d1654d46bfbf42ce81b72eaaba0e Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Sat, 24 Oct 2026 02:31:37 +0530 Subject: [PATCH 11/44] fix: no gap above the pinned results header --- CHANGELOG.md | 1 + packages/extension/static/styles/panel.css | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21b02b7..02f707e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ All notable changes to MeterX are documented here. The format follows [Keep a Ch ### Fixed - Empty figures show a short muted bar with the unit hidden, instead of a stray hyphen next to "Mbps" - The side panel no longer scrolls when an ISP plan is set: plan rows show the percentage on one line, the status moved into the Results header, and the chart shrinks on shorter panels +- Rows no longer show above the pinned column names while the results list scrolls - Down, Up and Ping columns in the side panel results have even spacing - Same-day charts label the axis with times instead of repeating the date, in the browser's own 12 or 24 hour format - Exporting a PDF no longer restyles the popup while the report renders, and the report no longer ends with a blank page diff --git a/packages/extension/static/styles/panel.css b/packages/extension/static/styles/panel.css index e96654b..433bf9c 100644 --- a/packages/extension/static/styles/panel.css +++ b/packages/extension/static/styles/panel.css @@ -77,7 +77,9 @@ .panel main { flex: 1; min-height: 0; gap: 10px; grid-template-rows: auto auto auto auto minmax(132px, 1fr); } .results-card { display: flex; flex-direction: column; min-height: 0; } .results-body { flex: 1; min-height: 0; overflow-y: auto; padding-top: 0; margin-top: 10px; } - .results thead th { position: sticky; top: 0; z-index: 1; background: var(--surface); } + /* No padding above the pinned names, or rows show through the gap while scrolling. */ + .panel .card-body.results-body { padding-top: 0; } + .results thead th { position: sticky; top: 0; z-index: 1; background: var(--surface); padding-top: 4px; box-shadow: 0 1px 0 var(--rule); } } /* Thin scrollbars in the theme's colours instead of the platform's grey bars. */ From 47f445d539265679d2895f3e444126a6153cdf10 Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Sat, 24 Oct 2026 02:33:48 +0530 Subject: [PATCH 12/44] refactor: share quality rules with the site, let the worker answer the site Quality grades, use-case scoring and their words move to @meterx/shared so the product site and the extension use one copy. The worker's CORS now also answers https://meterx.ajithakdev.com and localhost dev ports, for the site's in-page quick test. --- packages/extension/src/lib/quality.ts | 48 ++-------------------- packages/extension/src/lib/ui.ts | 10 ++--- packages/shared/src/index.ts | 9 ++++ packages/shared/src/origin.test.ts | 19 +++++++++ packages/shared/src/quality.ts | 59 +++++++++++++++++++++++++++ packages/shared/src/share.ts | 2 +- packages/worker/src/index.ts | 4 +- 7 files changed, 96 insertions(+), 55 deletions(-) create mode 100644 packages/shared/src/origin.test.ts create mode 100644 packages/shared/src/quality.ts diff --git a/packages/extension/src/lib/quality.ts b/packages/extension/src/lib/quality.ts index 6033792..1deaab4 100644 --- a/packages/extension/src/lib/quality.ts +++ b/packages/extension/src/lib/quality.ts @@ -1,27 +1,11 @@ /** - * Quality grade and use-case readiness. The one copy of these rules (#67). + * Quality rules live in @meterx/shared so the site uses the same ones; this file adds the toolbar badge colours. * @license AGPL-3.0 */ -import type { HistoryEntry, Quality } from '@meterx/shared'; +import type { Quality } from '@meterx/shared'; -export interface QualityLabel { - label: string; - cls: Quality; -} - -export function getQuality(dl: number, ul: number, ping: number): QualityLabel { - if (dl >= 50 && ul >= 20 && ping < 30) return { label: 'Excellent', cls: 'excellent' }; - if (dl >= 20 && ul >= 10 && ping < 60) return { label: 'Good', cls: 'good' }; - if (dl >= 5 && ul >= 2 && ping < 150) return { label: 'Fair', cls: 'fair' }; - return { label: 'Poor', cls: 'poor' }; -} - -/** Grade of a stored result, or null when it is missing a measurement. */ -export function qualityOf(e: Pick | undefined): QualityLabel | null { - if (!e || e.downloadSpeed === undefined || e.uploadSpeed === undefined || e.ping === undefined) return null; - return getQuality(e.downloadSpeed, e.uploadSpeed, e.ping); -} +export { getQuality, qualityOf, scoreUseCases, USE_CASES, type QualityLabel, type UseCaseScore, type UseCaseScores } from '@meterx/shared'; /** Toolbar badge colours. Kept in sync with the --q-* tokens in static/styles/tokens.css. */ export const QUALITY_COLORS: Record = { @@ -30,29 +14,3 @@ export const QUALITY_COLORS: Record = { fair: '#c27c0e', poor: '#d64545', }; - -export type UseCaseScore = 'good' | 'okay' | 'poor'; - -export interface UseCaseScores { - calls: UseCaseScore; - stream: UseCaseScore; - gaming: UseCaseScore; - upload: UseCaseScore; -} - -export function scoreUseCases( - dl: number, - ul: number, - ping: number, - jitter?: number, - loss?: number, -): UseCaseScores { - const j = jitter ?? 999; - const l = loss ?? 100; - return { - calls: (dl >= 5 && ul >= 3 && ping < 100 && j < 30) ? 'good' : (dl >= 2 && ul >= 1 && ping < 200) ? 'okay' : 'poor', - stream: dl >= 25 ? 'good' : dl >= 5 ? 'okay' : 'poor', - gaming: (ping < 50 && j < 20 && l < 1) ? 'good' : (ping < 100 && j < 40 && l < 3) ? 'okay' : 'poor', - upload: ul >= 10 ? 'good' : ul >= 3 ? 'okay' : 'poor', - }; -} diff --git a/packages/extension/src/lib/ui.ts b/packages/extension/src/lib/ui.ts index c87c3dd..1943f56 100644 --- a/packages/extension/src/lib/ui.ts +++ b/packages/extension/src/lib/ui.ts @@ -5,7 +5,9 @@ import type { Grade, HistoryEntry, IspPlan, Phase } from '@meterx/shared'; import { NONE, fmt } from './format'; -import { qualityOf, scoreUseCases, type UseCaseScore, type UseCaseScores } from './quality'; +import { USE_CASES, qualityOf, scoreUseCases, type UseCaseScores } from './quality'; + +export { USE_CASES }; // Where each phase starts on a progress bar and how long it usually takes. const PHASE_PLAN: Partial> = { @@ -56,12 +58,6 @@ export function setFigure(el: HTMLElement, value: string): void { el.classList.toggle('dash', value === NONE); } -export const USE_CASES: Array<{ key: keyof UseCaseScores; label: string; words: Record }> = [ - { key: 'calls', label: 'Video calls', words: { good: 'Smooth', okay: 'Choppy', poor: 'Too slow' } }, - { key: 'stream', label: 'Streaming', words: { good: '4K', okay: 'HD only', poor: 'Too slow' } }, - { key: 'gaming', label: 'Gaming', words: { good: 'Smooth', okay: 'May lag', poor: 'Laggy' } }, - { key: 'upload', label: 'Uploads', words: { good: 'Fast', okay: 'Slow', poor: 'Very slow' } }, -]; export function useCasesOf(e: HistoryEntry | undefined): UseCaseScores | null { if (!e || e.downloadSpeed === undefined || e.uploadSpeed === undefined || e.ping === undefined) return null; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index b0b0aae..d170a1b 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -16,6 +16,14 @@ export const TEST_FILE_RE = /^(1|5|10|25)MB\.bin$/; /** Bodies above this are cut off by both backends; the extension never sends more than 8 MB. */ export const MAX_UPLOAD_BYTES = 16 * 1024 * 1024; +/** The product site, which runs a short in-page test against the worker. */ +export const SITE_ORIGIN = 'https://meterx.ajithakdev.com'; + +/** Origins the backends answer with CORS: extensions, the site, and the site's local dev server. */ +export function isAllowedOrigin(origin: string | null | undefined): boolean { + return isExtensionOrigin(origin) || origin === SITE_ORIGIN || /^http:\/\/localhost:\d+$/.test(origin ?? ''); +} + /** Browser extension origins (Chrome, Edge, Opera, Firefox, Safari). Never `null`. */ export function isExtensionOrigin(origin: string | null | undefined): boolean { return !!origin && /^(chrome|moz|safari-web)-extension:\/\/[\w.-]+$/.test(origin); @@ -95,3 +103,4 @@ export type BroadcastMessage = | { action: 'testError'; error: string; cancelled?: boolean }; export { SHARE_VIEWER, buildShareUrl, parseShareUrl } from './share'; +export { getQuality, qualityOf, scoreUseCases, USE_CASES, type QualityLabel, type UseCaseScore, type UseCaseScores } from './quality'; diff --git a/packages/shared/src/origin.test.ts b/packages/shared/src/origin.test.ts new file mode 100644 index 0000000..a160e26 --- /dev/null +++ b/packages/shared/src/origin.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from 'vitest'; +import { isAllowedOrigin } from './index'; + +describe('isAllowedOrigin', () => { + it('allows extensions, the site and its local dev server', () => { + expect(isAllowedOrigin('chrome-extension://abcdefghijklmnop')).toBe(true); + expect(isAllowedOrigin('moz-extension://1234-5678')).toBe(true); + expect(isAllowedOrigin('https://meterx.ajithakdev.com')).toBe(true); + expect(isAllowedOrigin('http://localhost:8788')).toBe(true); + }); + it('refuses other sites, look-alikes and null', () => { + expect(isAllowedOrigin('https://evil.example')).toBe(false); + expect(isAllowedOrigin('https://meterx.ajithakdev.com.evil.example')).toBe(false); + expect(isAllowedOrigin('http://meterx.ajithakdev.com')).toBe(false); + expect(isAllowedOrigin('http://localhost.evil.example:80')).toBe(false); + expect(isAllowedOrigin('null')).toBe(false); + expect(isAllowedOrigin(null)).toBe(false); + }); +}); diff --git a/packages/shared/src/quality.ts b/packages/shared/src/quality.ts new file mode 100644 index 0000000..83258d5 --- /dev/null +++ b/packages/shared/src/quality.ts @@ -0,0 +1,59 @@ +/** + * Quality grade and use-case readiness. The one copy of these rules (#67). + * @license AGPL-3.0 + */ + +import type { HistoryEntry, Quality } from './index'; + +export interface QualityLabel { + label: string; + cls: Quality; +} + +export function getQuality(dl: number, ul: number, ping: number): QualityLabel { + if (dl >= 50 && ul >= 20 && ping < 30) return { label: 'Excellent', cls: 'excellent' }; + if (dl >= 20 && ul >= 10 && ping < 60) return { label: 'Good', cls: 'good' }; + if (dl >= 5 && ul >= 2 && ping < 150) return { label: 'Fair', cls: 'fair' }; + return { label: 'Poor', cls: 'poor' }; +} + +/** Grade of a stored result, or null when it is missing a measurement. */ +export function qualityOf(e: Pick | undefined): QualityLabel | null { + if (!e || e.downloadSpeed === undefined || e.uploadSpeed === undefined || e.ping === undefined) return null; + return getQuality(e.downloadSpeed, e.uploadSpeed, e.ping); +} + + +export type UseCaseScore = 'good' | 'okay' | 'poor'; + +export interface UseCaseScores { + calls: UseCaseScore; + stream: UseCaseScore; + gaming: UseCaseScore; + upload: UseCaseScore; +} + +export function scoreUseCases( + dl: number, + ul: number, + ping: number, + jitter?: number, + loss?: number, +): UseCaseScores { + const j = jitter ?? 999; + const l = loss ?? 100; + return { + calls: (dl >= 5 && ul >= 3 && ping < 100 && j < 30) ? 'good' : (dl >= 2 && ul >= 1 && ping < 200) ? 'okay' : 'poor', + stream: dl >= 25 ? 'good' : dl >= 5 ? 'okay' : 'poor', + gaming: (ping < 50 && j < 20 && l < 1) ? 'good' : (ping < 100 && j < 40 && l < 3) ? 'okay' : 'poor', + upload: ul >= 10 ? 'good' : ul >= 3 ? 'okay' : 'poor', + }; +} + +/** Plain words for each use case, shared by the extension, its exports and the site. */ +export const USE_CASES: Array<{ key: keyof UseCaseScores; label: string; words: Record }> = [ + { key: 'calls', label: 'Video calls', words: { good: 'Smooth', okay: 'Choppy', poor: 'Too slow' } }, + { key: 'stream', label: 'Streaming', words: { good: '4K', okay: 'HD only', poor: 'Too slow' } }, + { key: 'gaming', label: 'Gaming', words: { good: 'Smooth', okay: 'May lag', poor: 'Laggy' } }, + { key: 'upload', label: 'Uploads', words: { good: 'Fast', okay: 'Slow', poor: 'Very slow' } }, +]; diff --git a/packages/shared/src/share.ts b/packages/shared/src/share.ts index 27883e8..b1fe6ce 100644 --- a/packages/shared/src/share.ts +++ b/packages/shared/src/share.ts @@ -6,7 +6,7 @@ import type { Grade, HistoryEntry } from './index'; -export const SHARE_VIEWER = 'https://meterx.ajithakdev.com/r/'; +export const SHARE_VIEWER = 'https://meterx.ajithakdev.com/r/'; // SITE_ORIGIN + '/r/' type Shared = Pick; diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 7a496a6..76f4a3b 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -6,7 +6,7 @@ */ import { - VERSION, TEST_FILE_RE, MAX_UPLOAD_BYTES, isExtensionOrigin, ipFamily, + VERSION, TEST_FILE_RE, MAX_UPLOAD_BYTES, isAllowedOrigin, ipFamily, type HealthResponse, type IpInfo, } from '@meterx/shared'; @@ -30,7 +30,7 @@ function corsHeaders(request: Request): Record { 'Cache-Control': 'no-store', Vary: 'Origin', }; - if (isExtensionOrigin(origin)) { + if (isAllowedOrigin(origin)) { headers['Access-Control-Allow-Origin'] = origin!; // Lets the extension read DNS and connect timings (Resource Timing API). headers['Timing-Allow-Origin'] = origin!; From b443639de9340c066f640abe6778041534275927 Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Sat, 24 Oct 2026 02:48:15 +0530 Subject: [PATCH 13/44] feat: new logo and brand assets, product site for meterx.ajithakdev.com The X-on-a-dial mark replaces the gauge in the toolbar icon, the app header, the PDF and the site. brand/build.mjs generates icons, store tiles, marquee, social cards and store screenshots from it; earlier options are kept in brand/alternates. packages/site is the product site: a home page with a live quick test, a speed explorer, real screenshots, the test phases, install links and answers, plus about, privacy, the share viewer and /get. --- .gitignore | 1 + CHANGELOG.md | 4 + brand/alternates/chosen-x-on-dial-sheet.png | Bin 0 -> 71524 bytes brand/alternates/rising-bars-sheet.png | Bin 0 -> 56904 bytes brand/alternates/rising-bars.svg | 1 + brand/alternates/up-down-x-sheet.png | Bin 0 -> 63149 bytes brand/alternates/up-down-x.svg | 1 + brand/build.mjs | 160 ++++++++++++++ brand/png/icons/apple-touch-180.png | Bin 0 -> 3988 bytes brand/png/icons/icon-128.png | Bin 0 -> 4740 bytes brand/png/icons/icon-16.png | Bin 0 -> 590 bytes brand/png/icons/icon-256.png | Bin 0 -> 9563 bytes brand/png/icons/icon-32.png | Bin 0 -> 1303 bytes brand/png/icons/icon-48.png | Bin 0 -> 1980 bytes brand/png/icons/icon-512.png | Bin 0 -> 20767 bytes brand/png/icons/icon-96.png | Bin 0 -> 3774 bytes brand/png/icons/maskable-512.png | Bin 0 -> 11658 bytes brand/png/lockup-on-dark.png | Bin 0 -> 20025 bytes brand/png/lockup-on-light.png | Bin 0 -> 21610 bytes brand/png/social/github-1280x640.png | Bin 0 -> 41897 bytes brand/png/social/og-1200x630.png | Bin 0 -> 45097 bytes brand/png/store/edge-logo-300.png | Bin 0 -> 11403 bytes brand/png/store/marquee-1400x560.png | Bin 0 -> 83445 bytes brand/png/store/promo-440x280.png | Bin 0 -> 15099 bytes brand/png/store/screenshot-1-1280x800.png | Bin 0 -> 98544 bytes brand/png/store/screenshot-2-1280x800.png | Bin 0 -> 109438 bytes brand/png/store/screenshot-3-1280x800.png | Bin 0 -> 98840 bytes brand/png/store/screenshot-4-1280x800.png | Bin 0 -> 86065 bytes brand/png/store/screenshot-5-1280x800.png | Bin 0 -> 108818 bytes brand/png/ui/live-light.png | Bin 0 -> 167069 bytes brand/png/ui/panel-light.png | Bin 0 -> 181177 bytes brand/png/ui/popup-dark.png | Bin 0 -> 112393 bytes brand/png/ui/popup-light.png | Bin 0 -> 105289 bytes brand/png/ui/report.png | Bin 0 -> 156236 bytes brand/png/ui/settings-light.png | Bin 0 -> 194554 bytes brand/svg/glyph-on-dark.svg | 1 + brand/svg/glyph-on-light.svg | 1 + brand/svg/mark-light.svg | 1 + brand/svg/mark.svg | 1 + brand/svg/maskable.svg | 1 + docs/STATE.md | 28 +-- package-lock.json | 11 + packages/extension/src/background/index.ts | 2 +- packages/extension/src/lib/export-pdf.ts | 31 +-- packages/extension/src/lib/ui.ts | 4 +- packages/extension/static/images/icon-128.png | Bin 0 -> 4740 bytes packages/extension/static/images/icon-16.png | Bin 0 -> 590 bytes packages/extension/static/images/icon-32.png | Bin 0 -> 1303 bytes packages/extension/static/images/icon-48.png | Bin 0 -> 1980 bytes packages/extension/static/images/icons128.png | Bin 4038 -> 0 bytes packages/extension/static/images/icons16.png | Bin 418 -> 0 bytes packages/extension/static/images/icons48.png | Bin 1134 -> 0 bytes packages/extension/static/manifest.json | 14 +- packages/extension/static/options.html | 2 +- packages/site/build.mjs | 28 +++ packages/site/package.json | 14 ++ packages/site/src/capacity.test.ts | 28 +++ packages/site/src/capacity.ts | 29 +++ packages/site/src/common.ts | 34 +++ packages/site/src/get.ts | 11 + packages/site/src/home.ts | 156 ++++++++++++++ packages/site/src/share.ts | 37 ++++ packages/site/src/stores.test.ts | 12 ++ packages/site/src/stores.ts | 22 ++ packages/site/static/404.html | 48 +++++ packages/site/static/_headers | 11 + packages/site/static/about/index.html | 61 ++++++ packages/site/static/get/index.html | 56 +++++ packages/site/static/img/apple-touch-icon.png | Bin 0 -> 3988 bytes packages/site/static/img/icon-32.png | Bin 0 -> 1303 bytes packages/site/static/img/mark.svg | 1 + packages/site/static/img/maskable-512.png | Bin 0 -> 11658 bytes packages/site/static/img/og.png | Bin 0 -> 45097 bytes .../site/static/img/shot-history-dark.png | Bin 0 -> 59522 bytes .../site/static/img/shot-history-light.png | Bin 0 -> 56986 bytes packages/site/static/img/shot-load-dark.png | Bin 0 -> 23727 bytes packages/site/static/img/shot-load-light.png | Bin 0 -> 22615 bytes packages/site/static/img/shot-plan-dark.png | Bin 0 -> 26709 bytes packages/site/static/img/shot-plan-light.png | Bin 0 -> 24343 bytes packages/site/static/img/shot-report.png | Bin 0 -> 156236 bytes .../site/static/img/shot-settings-dark.png | Bin 0 -> 54536 bytes .../site/static/img/shot-settings-light.png | Bin 0 -> 51834 bytes packages/site/static/index.html | 160 ++++++++++++++ packages/site/static/privacy/index.html | 78 +++++++ packages/site/static/r/index.html | 70 ++++++ packages/site/static/robots.txt | 4 + packages/site/static/sitemap.xml | 6 + packages/site/static/styles/site.css | 199 ++++++++++++++++++ packages/site/tsconfig.json | 13 ++ 89 files changed, 1307 insertions(+), 35 deletions(-) create mode 100644 brand/alternates/chosen-x-on-dial-sheet.png create mode 100644 brand/alternates/rising-bars-sheet.png create mode 100644 brand/alternates/rising-bars.svg create mode 100644 brand/alternates/up-down-x-sheet.png create mode 100644 brand/alternates/up-down-x.svg create mode 100644 brand/build.mjs create mode 100644 brand/png/icons/apple-touch-180.png create mode 100644 brand/png/icons/icon-128.png create mode 100644 brand/png/icons/icon-16.png create mode 100644 brand/png/icons/icon-256.png create mode 100644 brand/png/icons/icon-32.png create mode 100644 brand/png/icons/icon-48.png create mode 100644 brand/png/icons/icon-512.png create mode 100644 brand/png/icons/icon-96.png create mode 100644 brand/png/icons/maskable-512.png create mode 100644 brand/png/lockup-on-dark.png create mode 100644 brand/png/lockup-on-light.png create mode 100644 brand/png/social/github-1280x640.png create mode 100644 brand/png/social/og-1200x630.png create mode 100644 brand/png/store/edge-logo-300.png create mode 100644 brand/png/store/marquee-1400x560.png create mode 100644 brand/png/store/promo-440x280.png create mode 100644 brand/png/store/screenshot-1-1280x800.png create mode 100644 brand/png/store/screenshot-2-1280x800.png create mode 100644 brand/png/store/screenshot-3-1280x800.png create mode 100644 brand/png/store/screenshot-4-1280x800.png create mode 100644 brand/png/store/screenshot-5-1280x800.png create mode 100644 brand/png/ui/live-light.png create mode 100644 brand/png/ui/panel-light.png create mode 100644 brand/png/ui/popup-dark.png create mode 100644 brand/png/ui/popup-light.png create mode 100644 brand/png/ui/report.png create mode 100644 brand/png/ui/settings-light.png create mode 100644 brand/svg/glyph-on-dark.svg create mode 100644 brand/svg/glyph-on-light.svg create mode 100644 brand/svg/mark-light.svg create mode 100644 brand/svg/mark.svg create mode 100644 brand/svg/maskable.svg create mode 100644 packages/extension/static/images/icon-128.png create mode 100644 packages/extension/static/images/icon-16.png create mode 100644 packages/extension/static/images/icon-32.png create mode 100644 packages/extension/static/images/icon-48.png delete mode 100644 packages/extension/static/images/icons128.png delete mode 100644 packages/extension/static/images/icons16.png delete mode 100644 packages/extension/static/images/icons48.png create mode 100644 packages/site/build.mjs create mode 100644 packages/site/package.json create mode 100644 packages/site/src/capacity.test.ts create mode 100644 packages/site/src/capacity.ts create mode 100644 packages/site/src/common.ts create mode 100644 packages/site/src/get.ts create mode 100644 packages/site/src/home.ts create mode 100644 packages/site/src/share.ts create mode 100644 packages/site/src/stores.test.ts create mode 100644 packages/site/src/stores.ts create mode 100644 packages/site/static/404.html create mode 100644 packages/site/static/_headers create mode 100644 packages/site/static/about/index.html create mode 100644 packages/site/static/get/index.html create mode 100644 packages/site/static/img/apple-touch-icon.png create mode 100644 packages/site/static/img/icon-32.png create mode 100644 packages/site/static/img/mark.svg create mode 100644 packages/site/static/img/maskable-512.png create mode 100644 packages/site/static/img/og.png create mode 100644 packages/site/static/img/shot-history-dark.png create mode 100644 packages/site/static/img/shot-history-light.png create mode 100644 packages/site/static/img/shot-load-dark.png create mode 100644 packages/site/static/img/shot-load-light.png create mode 100644 packages/site/static/img/shot-plan-dark.png create mode 100644 packages/site/static/img/shot-plan-light.png create mode 100644 packages/site/static/img/shot-report.png create mode 100644 packages/site/static/img/shot-settings-dark.png create mode 100644 packages/site/static/img/shot-settings-light.png create mode 100644 packages/site/static/index.html create mode 100644 packages/site/static/privacy/index.html create mode 100644 packages/site/static/r/index.html create mode 100644 packages/site/static/robots.txt create mode 100644 packages/site/static/sitemap.xml create mode 100644 packages/site/static/styles/site.css create mode 100644 packages/site/tsconfig.json diff --git a/.gitignore b/.gitignore index cb50820..d44e87e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ graphify-out/ # taste-skill working files (local) .taste-skill/ +packages/site/dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 02f707e..2398bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ All notable changes to MeterX are documented here. The format follows [Keep a Ch - Running a test from the side panel shows it live: the summary card switches to the current download, upload, ping and elapsed time, the heading names the phase, and a progress line runs under the header - Extension name is now "MeterX by ajithakdev", and empty values show a plain hyphen +- New logo: a dial with download and upload arrows crossing into an X, on the toolbar icon, in the app, the PDF and the site. Store images, social cards and icons are generated by `brand/build.mjs` +- Product site in `packages/site` for meterx.ajithakdev.com: home page with an in-page quick test, about, privacy policy (#92), share viewer (#74) and a `/get` link that opens the right store +- The worker answers the site's origin so its quick test can run + ### Fixed - Empty figures show a short muted bar with the unit hidden, instead of a stray hyphen next to "Mbps" - The side panel no longer scrolls when an ISP plan is set: plan rows show the percentage on one line, the status moved into the Results header, and the chart shrinks on shorter panels diff --git a/brand/alternates/chosen-x-on-dial-sheet.png b/brand/alternates/chosen-x-on-dial-sheet.png new file mode 100644 index 0000000000000000000000000000000000000000..ed1424b6f24a7a04dfcbe0fad54ec093ddf0beac GIT binary patch literal 71524 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�HAWcx8~e3rtXl!=dz88Rd1rHPJ4 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 literal 0 HcmV?d00001 diff --git a/brand/alternates/rising-bars-sheet.png b/brand/alternates/rising-bars-sheet.png new file mode 100644 index 0000000000000000000000000000000000000000..9eaf21ca41557fe968e6e8f6cb2155a026591e87 GIT binary patch literal 56904 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 z \ No newline at end of file diff --git a/brand/alternates/up-down-x-sheet.png b/brand/alternates/up-down-x-sheet.png new file mode 100644 index 0000000000000000000000000000000000000000..9ebadec562c05893dcdbdaa2fece8f4d674c61bf GIT binary patch literal 63149 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≺^ZQyKFxKI#ltXF3u1p#n2wm}c8yg$3 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 I)wd7%H*U~0tpET3 literal 0 HcmV?d00001 diff --git a/brand/alternates/up-down-x.svg b/brand/alternates/up-down-x.svg new file mode 100644 index 0000000..0c10820 --- /dev/null +++ b/brand/alternates/up-down-x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/brand/build.mjs b/brand/build.mjs new file mode 100644 index 0000000..e6598da --- /dev/null +++ b/brand/build.mjs @@ -0,0 +1,160 @@ +/** + * Builds every MeterX brand asset from the mark defined below: SVG masters, extension and site icons, + * store logo, promo tiles, marquee, social cards and store screenshots. + * + * Needs a built extension (npm run build) and a Chromium with Playwright: + * PLAYWRIGHT= CHROMIUM= node brand/build.mjs + * @license AGPL-3.0 + */ + +import { mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'fs'; +import { dirname, resolve } from 'path'; +import { fileURLToPath, pathToFileURL } from 'url'; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = resolve(here, '..'); +const { chromium } = await import(process.env.PLAYWRIGHT ? pathToFileURL(process.env.PLAYWRIGHT).href : 'playwright'); +const out = (...p) => { const f = resolve(here, ...p); mkdirSync(dirname(f), { recursive: true }); return f; }; + +// --- The mark: a three-quarter dial with download and upload arrows crossing into an X. +const INK = { dark: '#0b0c0e', light: '#ffffff' }; +const COLOURS = { + onDark: { dial: '#3a3e44', up: '#f4f5f6', down: '#74a6ff' }, + onLight: { dial: '#cdd2d8', up: '#0a0a0a', down: '#1d5fbf' }, +}; + +/** The glyph in a 128 box. The upload arrow cuts a gap through the download arrow with a mask, so it works on any background. */ +function glyph(c, id) { + return `` + + `` + + `` + + ``; +} +const svg = (inner, box = 128) => `${inner}\n`; +const SVG = { + 'mark.svg': svg(`${glyph(COLOURS.onDark, 'm')}`), + 'mark-light.svg': svg(`${glyph(COLOURS.onLight, 'm')}`), + 'glyph-on-dark.svg': svg(glyph(COLOURS.onDark, 'g')), + 'glyph-on-light.svg': svg(glyph(COLOURS.onLight, 'g')), + // Maskable: full-bleed square, glyph inside the 80% safe zone. + 'maskable.svg': svg(`${glyph(COLOURS.onDark, 'k')}`), +}; +for (const [name, body] of Object.entries(SVG)) writeFileSync(out('svg', name), body); + +const uri = s => `data:image/svg+xml;base64,${Buffer.from(s).toString('base64')}`; +const font = readFileSync(resolve(root, 'packages/extension/static/fonts/Geist-Variable.woff2')).toString('base64'); +const page = (w, h, bg, body, css = '') => `${body}`; + +const browser = await chromium.launch({ executablePath: process.env.CHROMIUM || undefined }); +async function render(html, w, h, file, transparent = false) { + const p = await browser.newPage({ viewport: { width: w, height: h } }); + await p.setContent(html); + await p.evaluate(() => document.fonts.ready); + await p.screenshot({ path: out(file), omitBackground: transparent }); + await p.close(); +} +const img = (s, size) => ``; + +// --- Icons +for (const size of [16, 32, 48, 96, 128, 256, 512]) await render(page(size, size, 'transparent', img(SVG['mark.svg'], size)), size, size, `png/icons/icon-${size}.png`, true); +await render(page(512, 512, INK.dark, img(SVG['maskable.svg'], 512)), 512, 512, 'png/icons/maskable-512.png'); +await render(page(180, 180, INK.dark, img(SVG['maskable.svg'], 180)), 180, 180, 'png/icons/apple-touch-180.png'); +await render(page(300, 300, 'transparent', img(SVG['mark.svg'], 300)), 300, 300, 'png/store/edge-logo-300.png', true); + +// --- Wordmark lockups +const lockup = (theme) => { + const dark = theme === 'dark'; + return page(1200, 400, dark ? INK.dark : '#f2f4f7', `
      ${img(SVG[dark ? 'mark.svg' : 'mark-light.svg'], 200)}MeterX
      `, + `.l{height:100%;display:flex;align-items:center;justify-content:center;gap:48px}.l span{font-size:150px;font-weight:600;letter-spacing:-.04em;color:${dark ? '#f4f5f6' : '#0a0a0a'}}${dark ? '' : '.l img{box-shadow:0 0 0 2px #e1e4e8;border-radius:44px}'}`); +}; +await render(lockup('dark'), 1200, 400, 'png/lockup-on-dark.png'); +await render(lockup('light'), 1200, 400, 'png/lockup-on-light.png'); + +// --- Real interface captures from the built extension, with sample results (a 300/100 plan, a week of tests). +const dist = resolve(root, 'packages/extension/dist'); +const now = Date.now() - 30 * 60e3; +let seed = 11; +const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647; +const hist = Array.from({ length: 30 }, (_, i) => { + const ping = 18 + rnd() * 8, dl = 262 + rnd() * 40 - (i === 9 ? 120 : 0); + return { timestamp: now - i * 5.6 * 3600e3, downloadSpeed: dl, uploadSpeed: 88 + rnd() * 10, ping, jitter: 1.5 + rnd() * 2.5, packetLoss: 0, + bufferbloatDelta: 30 + rnd() * 20, bufferbloatGrade: 'B', loadedPingDown: ping + 42, loadedPingUp: ping + 27, ipv4Ok: true, ipv6Ok: true, edge: 'MAA', isp: 'Airtel' }; +}); +const sync = { ispPlan: { downloadMbps: 300, uploadMbps: 100, providerName: 'Airtel' } }; +const stub = `window.__l=[];window.chrome={storage:{sync:{get:async(k)=>{const s=${JSON.stringify(sync)};return typeof k==='string'?{[k]:s[k]}:s},set:async()=>{}},` + + `local:{get:async()=>({history:${JSON.stringify(hist)}}),set:async()=>{}},onChanged:{addListener(){}}},` + + `runtime:{sendMessage:async(m)=>m&&m.action==='getState'?null:{ok:true,ping:19,edge:'MAA'},onMessage:{addListener(f){__l.push(f)}},getURL:x=>x,openOptionsPage(){}},` + + `permissions:{contains:async()=>true,request:async()=>true}};window.__send=m=>__l.forEach(f=>f(m));`; +async function capture(file, w, h, scheme, name, act) { + const p = await browser.newPage({ viewport: { width: w, height: h }, colorScheme: scheme, deviceScaleFactor: 2 }); + await p.addInitScript(stub); + await p.goto(pathToFileURL(resolve(dist, file)).href); + await p.waitForTimeout(600); + if (act) await act(p); + const height = await p.evaluate(() => Math.ceil(document.body.getBoundingClientRect().height)); + await p.screenshot({ path: out('png/ui', name), clip: { x: 0, y: 0, width: w, height: Math.min(h, height) } }); + await p.close(); +} +for (const scheme of ['dark', 'light']) await capture('popup.html', 360, 600, scheme, `popup-${scheme}.png`); +await capture('sidepanel.html', 420, 880, 'light', 'panel-light.png'); +await capture('sidepanel.html', 420, 880, 'light', 'live-light.png', async p => { + await p.click('#runBtn'); + for (const data of [{ phase: 'latency', ping: 19 }, { phase: 'download', downloadSpeed: 284.6 }, { phase: 'upload', uploadSpeed: 61.2 }]) { + await p.evaluate(d => window.__send({ action: 'testProgress', data: d }), data); + await p.waitForTimeout(900); + } +}); +await capture('options.html', 1000, 760, 'light', 'settings-light.png'); +if (process.env.REPORT_PNG) copyFileSync(process.env.REPORT_PNG, out('png/ui/report.png')); + +// --- Banners and social cards: dark ground, the mark large, the name and one line. +function banner(w, h, markSize, title, line, shot) { + const pad = Math.round(h * 0.17); + const ghost = ``; + return page(w, h, INK.dark, `${ghost}
      ${img(SVG['mark.svg'], markSize)}

      ${title}

      ${line}

      ${shot ? `` : ''}`, + `.b{position:absolute;left:${pad}px;top:0;bottom:0;display:flex;align-items:center;gap:${Math.round(markSize * .3)}px} + h1{font-size:${Math.round(h * .16)}px;font-weight:600;letter-spacing:-.04em;line-height:1;color:#f4f5f6} + p{font-size:${Math.round(h * .055)}px;line-height:1.3;color:#a4a8ae;margin-top:${Math.round(h * .03)}px;max-width:${shot ? 15 : 20}ch} + .ghost{position:absolute;right:${-h * .25}px;bottom:${-h * .35}px;width:${h * 1.15}px;height:${h * 1.15}px;opacity:.07} + .shot{position:absolute;right:${pad}px;top:50%;transform:translateY(-50%);height:${Math.round(h * .86)}px;border-radius:14px}`); +} +const popupDark = readFileSync(out('png/ui/popup-dark.png')).toString('base64'); +const popupShot = `data:image/png;base64,${popupDark}`; +await render(banner(440, 280, 76, 'MeterX', 'Speed, ping and bufferbloat from your toolbar.'), 440, 280, 'png/store/promo-440x280.png'); +await render(banner(1400, 560, 170, 'MeterX', 'Internet speed, ping and bufferbloat from your browser toolbar.', popupShot), 1400, 560, 'png/store/marquee-1400x560.png'); +await render(banner(1200, 630, 190, 'MeterX', 'Internet speed, ping and bufferbloat from your browser toolbar.'), 1200, 630, 'png/social/og-1200x630.png'); +await render(banner(1280, 640, 190, 'MeterX', 'Open-source speed test for the browser toolbar.'), 1280, 640, 'png/social/github-1280x640.png'); + +// --- Store screenshots, 1280 x 800: a caption on the left, the real interface on the right. +const shots = [ + ['popup-light.png', 'Your speed, in one click', 'Download, upload, ping, jitter and loss, with a plain answer about what the connection can handle.'], + ['panel-light.png', 'Every test, kept on your device', 'Averages, a trend chart and up to 200 results in the side panel.'], + ['live-light.png', 'Watch the test as it runs', 'Live figures and a progress line while each phase is measured.'], + ['settings-light.png', 'Your plan, your schedule, your server', 'Compare with the speeds you pay for, test on a timer, or use your own server.'], + ['report.png', 'Reports you can hand to your provider', 'A PDF with averages, a trend and every result, plus CSV and JSON.'], +]; +for (const [i, [file, title, line]] of shots.entries()) { + const data = readFileSync(out('png/ui', file)).toString('base64'); + await render(page(1280, 800, '#f2f4f7', `

      ${title}

      ${line}

      `, + `.c{position:absolute;left:80px;top:0;bottom:0;width:440px;display:flex;flex-direction:column;justify-content:center} + h1{font-size:52px;font-weight:600;letter-spacing:-.035em;line-height:1.02;color:#0a0a0a}p{font-size:22px;line-height:1.4;color:#5b6068;margin-top:20px} + .s{position:absolute;left:600px;right:60px;top:40px;bottom:40px;display:flex;align-items:center;justify-content:center} + .s img{max-width:100%;max-height:100%;border-radius:14px;box-shadow:0 0 0 1px #e1e4e8}`), 1280, 800, `png/store/screenshot-${i + 1}-1280x800.png`); +} + +await browser.close(); + +// --- Copy into the extension and the site. +const ext = resolve(root, 'packages/extension/static/images'); +mkdirSync(ext, { recursive: true }); +for (const s of [16, 32, 48, 128]) copyFileSync(out(`png/icons/icon-${s}.png`), resolve(ext, `icon-${s}.png`)); +const site = resolve(root, 'packages/site/static/img'); +mkdirSync(site, { recursive: true }); +copyFileSync(out('svg/mark.svg'), resolve(site, 'mark.svg')); +copyFileSync(out('png/icons/icon-32.png'), resolve(site, 'icon-32.png')); +copyFileSync(out('png/icons/apple-touch-180.png'), resolve(site, 'apple-touch-icon.png')); +copyFileSync(out('png/icons/maskable-512.png'), resolve(site, 'maskable-512.png')); +copyFileSync(out('png/social/og-1200x630.png'), resolve(site, 'og.png')); +console.log('Brand assets written to brand/ and copied into the extension and the site.'); diff --git a/brand/png/icons/apple-touch-180.png b/brand/png/icons/apple-touch-180.png new file mode 100644 index 0000000000000000000000000000000000000000..f1db1b9508461b4bef27fe6e2b78cb302e9611b6 GIT binary patch literal 3988 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
      @@ -49,9 +49,9 @@

      Who made it

      MeterX by Ajith Kumar K, an open-source internet speed test for the browser.

      Back to top
      - github.com/ajithakdev/meterx-server + github.com/ajithakdev/meterx diff --git a/apps/site/static/get/index.html b/apps/site/static/get/index.html index 3154a18..d41a141 100644 --- a/apps/site/static/get/index.html +++ b/apps/site/static/get/index.html @@ -43,9 +43,9 @@

      Choose your browser

      MeterX by Ajith Kumar K, an open-source internet speed test for the browser.

      Back to top
      - github.com/ajithakdev/meterx-server + github.com/ajithakdev/meterx diff --git a/apps/site/static/index.html b/apps/site/static/index.html index d71a788..375d79d 100644 --- a/apps/site/static/index.html +++ b/apps/site/static/index.html @@ -142,9 +142,9 @@

      Test from your toolbar.

      MeterX by Ajith Kumar K, an open-source internet speed test for the browser.

      Back to top
      - github.com/ajithakdev/meterx-server + github.com/ajithakdev/meterx diff --git a/apps/site/static/privacy/index.html b/apps/site/static/privacy/index.html index 967df2c..3ebe33b 100644 --- a/apps/site/static/privacy/index.html +++ b/apps/site/static/privacy/index.html @@ -58,7 +58,7 @@

      This website

      Children

      MeterX is not aimed at children and collects no personal data from anyone.

      Changes and contact

      -

      Changes to this policy are listed in the repository's history and the date above is updated. Questions go to the issue tracker or ajithkumarks2579@gmail.com.

      +

      Changes to this policy are listed in the repository's history and the date above is updated. Questions go to the issue tracker or ajithkumarks2579@gmail.com.

      @@ -66,9 +66,9 @@

      Changes and contact

      MeterX by Ajith Kumar K, an open-source internet speed test for the browser.

      Back to top
      - github.com/ajithakdev/meterx-server + github.com/ajithakdev/meterx diff --git a/apps/site/static/r/index.html b/apps/site/static/r/index.html index 53f0c98..b81ba6a 100644 --- a/apps/site/static/r/index.html +++ b/apps/site/static/r/index.html @@ -57,9 +57,9 @@

      Measured with MeterX

      MeterX by Ajith Kumar K, an open-source internet speed test for the browser.

      Back to top
      - github.com/ajithakdev/meterx-server + github.com/ajithakdev/meterx diff --git a/privacy-policy.md b/privacy-policy.md index ad2937d..8eaeee5 100644 --- a/privacy-policy.md +++ b/privacy-policy.md @@ -63,4 +63,4 @@ If this policy changes, the "Last Updated" date at the top will reflect it. - **Author:** Ajith Kumar K ([@ajithakdev](https://github.com/ajithakdev)) - **Email:** ajithkumarks2579@gmail.com -- **GitHub:** [github.com/ajithakdev/meterx-server/issues](https://github.com/ajithakdev/meterx-server/issues) +- **GitHub:** [github.com/ajithakdev/meterx/issues](https://github.com/ajithakdev/meterx/issues) From d659b5a616837d7f7bd4f7c912221fbde9a655ee Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Sat, 24 Oct 2026 14:42:52 +0530 Subject: [PATCH 18/44] refactor: move reports into @meterx/reports and pure helpers into @meterx/shared The PDF, CSV and JSON builders and their stats become @meterx/reports, a pure package the extension and the site can both use. Formatting, the median, the load grade and plan share move into @meterx/shared. The extension keeps only the browser parts: reading the saved plan and starting downloads. --- apps/extension/package.json | 2 +- apps/extension/src/background/measure.ts | 18 +- apps/extension/src/lib/export-data.ts | 83 +---- apps/extension/src/lib/export-pdf.ts | 351 +----------------- apps/extension/src/lib/format.ts | 41 +- apps/extension/src/lib/isp.ts | 6 +- package-lock.json | 16 +- packages/reports/package.json | 18 + .../reports/src/data.test.ts | 4 +- packages/reports/src/data.ts | 77 ++++ packages/reports/src/index.ts | 9 + .../reports/src/pdf.test.ts | 4 +- packages/reports/src/pdf.ts | 347 +++++++++++++++++ .../reports/src/stats.test.ts | 4 +- .../reports/src/stats.ts | 4 +- packages/reports/tsconfig.json | 15 + .../shared/src}/format.test.ts | 0 packages/shared/src/format.ts | 43 +++ packages/shared/src/index.ts | 2 + packages/shared/src/stats.ts | 27 ++ 20 files changed, 576 insertions(+), 495 deletions(-) create mode 100644 packages/reports/package.json rename apps/extension/src/lib/export-data.test.ts => packages/reports/src/data.test.ts (96%) create mode 100644 packages/reports/src/data.ts create mode 100644 packages/reports/src/index.ts rename apps/extension/src/lib/export-pdf.test.ts => packages/reports/src/pdf.test.ts (94%) create mode 100644 packages/reports/src/pdf.ts rename apps/extension/src/lib/report-stats.test.ts => packages/reports/src/stats.test.ts (94%) rename apps/extension/src/lib/report-stats.ts => packages/reports/src/stats.ts (93%) create mode 100644 packages/reports/tsconfig.json rename {apps/extension/src/lib => packages/shared/src}/format.test.ts (100%) create mode 100644 packages/shared/src/format.ts create mode 100644 packages/shared/src/stats.ts diff --git a/apps/extension/package.json b/apps/extension/package.json index 590c12d..c5d37bc 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -12,6 +12,6 @@ }, "dependencies": { "@meterx/shared": "*", - "jspdf": "^4.2.1" + "@meterx/reports": "*" } } diff --git a/apps/extension/src/background/measure.ts b/apps/extension/src/background/measure.ts index b6ce90b..17792aa 100644 --- a/apps/extension/src/background/measure.ts +++ b/apps/extension/src/background/measure.ts @@ -3,7 +3,9 @@ * @license AGPL-3.0 */ -import type { Grade } from '@meterx/shared'; +import { gradeBufferbloat, median } from '@meterx/shared'; + +export { gradeBufferbloat, median }; /** Cumulative bytes received at time t (ms since the phase started). */ export interface ByteSample { t: number; bytes: number } @@ -39,12 +41,6 @@ export function steadyMbps(samples: ByteSample[], elapsedMs: number, opts: { ram return toMbps(bytesAtTime(samples, end) - bytesAtTime(samples, opts.rampMs), end - opts.rampMs); } -export function median(values: number[]): number { - if (values.length === 0) return NaN; - const s = [...values].sort((a, b) => a - b); - const mid = s.length >> 1; - return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2; -} /** Median ping, mean absolute change between consecutive pings, and % of probes lost. */ export function latencyStats(latencies: number[], attempts: number): { ping: number; jitter: number; packetLoss: number } { @@ -57,14 +53,6 @@ export function latencyStats(latencies: number[], attempts: number): { ping: num }; } -/** How much latency rises under load. Thresholds follow the Waveform bufferbloat test. */ -export function gradeBufferbloat(deltaMs: number): Grade { - if (deltaMs <= 5) return 'A'; - if (deltaMs <= 30) return 'B'; - if (deltaMs <= 60) return 'C'; - if (deltaMs <= 200) return 'D'; - return 'F'; -} const KB = 1024; const MIN_UPLOAD_CHUNK = 256 * KB; diff --git a/apps/extension/src/lib/export-data.ts b/apps/extension/src/lib/export-data.ts index 475e961..2e0f155 100644 --- a/apps/extension/src/lib/export-data.ts +++ b/apps/extension/src/lib/export-data.ts @@ -1,15 +1,12 @@ /** - * CSV and JSON exports. One column list feeds both, so they always carry the same fields. + * CSV and JSON downloads. The files themselves are built by @meterx/reports. * @license AGPL-3.0 */ -import { VERSION, type HistoryEntry } from '@meterx/shared'; -import { gradeBufferbloat } from '../background/measure'; -import { localDate } from './format'; -import { qualityOf } from './quality'; -import { summarize, type Spread } from './report-stats'; +import { localDate, type HistoryEntry } from '@meterx/shared'; +import { toCsv, toJson } from '@meterx/reports'; -function triggerDownload(blob: Blob, filename: string): void { +export function triggerDownload(blob: Blob, filename: string): void { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -20,81 +17,11 @@ function triggerDownload(blob: Blob, filename: string): void { setTimeout(() => URL.revokeObjectURL(url), 5000); } -const round = (v: number | undefined, d: number) => (v === undefined || !Number.isFinite(v) ? undefined : Number(v.toFixed(d))); -const yesNo = (v: boolean | undefined) => (v === undefined ? undefined : v ? 'yes' : 'no'); -const localTime = (ts: number) => { - const d = new Date(ts); - return `${localDate(d)} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; -}; - -type Value = string | number | undefined; - -/** Export columns: JSON key, CSV header, value. Units live in both names. */ -export const COLUMNS: Array<{ key: string; header: string; get: (e: HistoryEntry) => Value }> = [ - { key: 'time', header: 'Time (UTC)', get: e => new Date(e.timestamp).toISOString() }, - { key: 'localTime', header: 'Local time', get: e => localTime(e.timestamp) }, - { key: 'downloadMbps', header: 'Download (Mbps)', get: e => round(e.downloadSpeed, 2) }, - { key: 'uploadMbps', header: 'Upload (Mbps)', get: e => round(e.uploadSpeed, 2) }, - { key: 'pingMs', header: 'Ping (ms)', get: e => round(e.ping, 1) }, - { key: 'jitterMs', header: 'Jitter (ms)', get: e => round(e.jitter, 1) }, - { key: 'packetLossPercent', header: 'Packet loss (%)', get: e => round(e.packetLoss, 1) }, - { key: 'pingWhileDownloadingMs', header: 'Ping while downloading (ms)', get: e => round(e.loadedPingDown, 1) }, - { key: 'pingWhileUploadingMs', header: 'Ping while uploading (ms)', get: e => round(e.loadedPingUp, 1) }, - { key: 'bufferbloatMs', header: 'Bufferbloat (ms)', get: e => round(e.bufferbloatDelta, 1) }, - { key: 'bufferbloatGrade', header: 'Bufferbloat grade', get: e => e.bufferbloatGrade ?? (e.bufferbloatDelta !== undefined ? gradeBufferbloat(e.bufferbloatDelta) : undefined) }, - { key: 'quality', header: 'Quality', get: e => qualityOf(e)?.label }, - { key: 'dnsLookupMs', header: 'DNS lookup (ms)', get: e => round(e.dnsLookupMs, 1) }, - { key: 'ipv4', header: 'IPv4', get: e => yesNo(e.ipv4Ok) }, - { key: 'ipv6', header: 'IPv6', get: e => yesNo(e.ipv6Ok) }, - { key: 'serverLocation', header: 'Server location', get: e => e.edge }, - { key: 'isp', header: 'ISP', get: e => e.isp }, -]; - - -/** Rows newest first, as stored. */ -export function toCsv(history: HistoryEntry[]): string { - const lines = [COLUMNS.map(c => escapeCsv(c.header)).join(',')]; - for (const e of history) lines.push(COLUMNS.map(c => escapeCsv(c.get(e))).join(',')); - return lines.join('\r\n') + '\r\n'; -} - export function exportCsv(history: HistoryEntry[]): void { // The BOM makes Excel read the file as UTF-8. - triggerDownload(new Blob(['', toCsv(history)], { type: 'text/csv;charset=utf-8' }), `meterx-results-${localDate()}.csv`); -} - -const typical = (s: Spread | null, k: 'avg' | 'median', d: number) => (s ? round(s[k], d) : undefined); - -export function toJson(history: HistoryEntry[], now = Date.now()): object { - const s = summarize(history); - return { - format: 'meterx-results', - formatVersion: 1, - app: { name: 'MeterX', version: VERSION }, - exportedAt: new Date(now).toISOString(), - period: history.length ? { from: new Date(s.from).toISOString(), to: new Date(s.to).toISOString(), tests: s.count } : null, - summary: { - downloadMbpsAverage: typical(s.download, 'avg', 2), - uploadMbpsAverage: typical(s.upload, 'avg', 2), - pingMsMedian: typical(s.ping, 'median', 1), - jitterMsMedian: typical(s.jitter, 'median', 1), - packetLossPercentAverage: typical(s.loss, 'avg', 1), - bufferbloatGrade: s.grade ?? undefined, - quality: s.quality?.label, - goodEnoughFor: s.useCases ?? undefined, - }, - results: history.map(e => Object.fromEntries(COLUMNS.map(c => [c.key, c.get(e)]).filter(([, v]) => v !== undefined))), - }; + triggerDownload(new Blob(['\uFEFF', toCsv(history)], { type: 'text/csv;charset=utf-8' }), `meterx-results-${localDate()}.csv`); } export function exportJson(history: HistoryEntry[]): void { triggerDownload(new Blob([JSON.stringify(toJson(history), null, 2)], { type: 'application/json' }), `meterx-results-${localDate()}.json`); } - -/** RFC 4180 quoting, plus a leading ' on text that a spreadsheet would run as a formula (#78). */ -export function escapeCsv(v: unknown): string { - if (v === null || v === undefined) return ''; - let s = String(v); - if (typeof v === 'string' && /^[=+\-@\t\r]/.test(s)) s = `'${s}`; - return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; -} diff --git a/apps/extension/src/lib/export-pdf.ts b/apps/extension/src/lib/export-pdf.ts index 1b8941d..4d0161c 100644 --- a/apps/extension/src/lib/export-pdf.ts +++ b/apps/extension/src/lib/export-pdf.ts @@ -1,35 +1,11 @@ /** - * PDF report, drawn as vector text and shapes with jsPDF (loaded on demand). - * Same design as the app: ink and grey on white, one blue for download, - * status colour only on pills. Colours mirror the light tokens in static/styles/app.css. + * PDF download: reads the saved plan, builds the report with @meterx/reports, saves it. * @license AGPL-3.0 */ -import { VERSION, type Grade, type HistoryEntry, type IspPlan } from '@meterx/shared'; -import type { jsPDF as JsPdf } from 'jspdf'; -import { NONE, fmt, fmtSpeed, localDate } from './format'; -import { loadIspPlan, planShare } from './isp'; -import { qualityOf, type UseCaseScore } from './quality'; -import { summarize, type Spread, type Summary } from './report-stats'; -import { USE_CASES } from './ui'; - -const C = { - text: '#0a0a0a', muted: '#5b6068', border: '#e1e4e8', rule: '#eceef1', raised: '#eaedf1', - data: '#1d5fbf', second: '#8a8f96', grid: '#e6e8eb', -}; -type Tone = 'positive' | 'warning' | 'negative' | 'neutral'; -const PILL: Record = { - positive: ['#dcfae5', '#04250e'], warning: ['#fff4d6', '#7a3a0b'], negative: ['#ffedea', '#831c1e'], neutral: ['#eaedf1', '#3d4148'], -}; -const scoreTone: Record = { good: 'positive', okay: 'warning', poor: 'negative' }; -const gradeTone = (g: Grade): Tone => (g === 'A' || g === 'B' ? 'positive' : g === 'C' ? 'warning' : 'negative'); -const qualityTone = (cls: string): Tone => (cls === 'excellent' || cls === 'good' ? 'positive' : cls === 'fair' ? 'warning' : 'negative'); - -const PAGE_W = 210, PAGE_H = 297, M = 16, W = PAGE_W - 2 * M, FOOT_Y = PAGE_H - 10, BOTTOM = FOOT_Y - 8; - -const day = (ts: number) => new Date(ts).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }); -const when = (ts: number) => `${new Date(ts).toLocaleDateString(undefined, { day: 'numeric', month: 'short' })} ${new Date(ts).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}`; -const range = (s: Spread | null, f: (v: number) => string) => (s && s.n > 1 ? `${f(s.min)} to ${f(s.max)}` : ''); +import { localDate, type HistoryEntry } from '@meterx/shared'; +import { buildReport } from '@meterx/reports'; +import { loadIspPlan } from './isp'; export async function exportPdfReport(hist: HistoryEntry[]): Promise { if (hist.length === 0) throw new Error('No data to export'); @@ -37,322 +13,3 @@ export async function exportPdfReport(hist: HistoryEntry[]): Promise { const doc = await buildReport(hist, plan); doc.save(`meterx-report-${localDate()}.pdf`); } - -/** Builds the report (history newest first, as stored). Split from the download so it can be tested. */ -export async function buildReport(hist: HistoryEntry[], plan: IspPlan | null, now = Date.now()): Promise { - const { jsPDF, GState } = await import('jspdf'); - const doc = new jsPDF({ unit: 'mm', format: 'a4' }); - const id = await reportId(hist); - doc.setProperties({ title: 'MeterX connection report', subject: `Report ID ${id}`, creator: `MeterX ${VERSION}`, keywords: `meterx ${id}` }); - const s = summarize(hist); - const d = new Draw(doc); - - let y = header(d, s, now); - y = intro(d, s, plan, y); - y = answer(d, s, y); - y = strip(d, s, y); - y = loadAndReady(d, s, y); - if (plan && (plan.downloadMbps > 0 || plan.uploadMbps > 0)) y = planCard(d, s, plan, y); - y = chart(d, hist, y); - results(d, hist, y); - footers(d, id, new GState({ opacity: 0.045 }), new GState({ opacity: 1 })); - return doc; -} - -/** - * Short fingerprint of the measurements (time and every figure), shown on each page. - * The same results always give the same ID, so a figure changed in an edited copy no longer matches - * a fresh export. shortcut: not a signature, anyone can recompute it; sign server-side if reports need proof. - */ -export async function reportId(hist: HistoryEntry[]): Promise { - const data = JSON.stringify(hist.map(e => [e.timestamp, e.downloadSpeed, e.uploadSpeed, e.ping, e.jitter, e.packetLoss, e.bufferbloatDelta])); - const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data))); - const hex = [...hash.slice(0, 8)].map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase(); - return hex.match(/.{4}/g)!.join('-'); -} - -/** Thin wrapper so every call sets font, size and colour together. */ -class Draw { - constructor(readonly doc: JsPdf) {} - text(t: string, x: number, y: number, o: { size?: number; bold?: boolean; color?: string; align?: 'left' | 'right' | 'center' } = {}): void { - this.doc.setFont('helvetica', o.bold ? 'bold' : 'normal'); - this.doc.setFontSize(o.size ?? 9); - this.doc.setTextColor(o.color ?? C.text); - this.doc.text(t, x, y, { align: o.align ?? 'left' }); - } - width(t: string, size: number, bold = false): number { - this.doc.setFont('helvetica', bold ? 'bold' : 'normal'); - this.doc.setFontSize(size); - return this.doc.getTextWidth(t); - } - card(x: number, y: number, w: number, h: number): void { - this.doc.setDrawColor(C.border); - this.doc.setLineWidth(0.25); - this.doc.roundedRect(x, y, w, h, 2, 2, 'S'); - } - rule(x1: number, y: number, x2: number, color = C.rule): void { - this.doc.setDrawColor(color); - this.doc.setLineWidth(0.25); - this.doc.line(x1, y, x2, y); - } - bar(x: number, y: number, w: number, h: number, color: string): void { - if (w <= 0) return; - this.doc.setFillColor(color); - this.doc.roundedRect(x, y, w, h, h / 2, h / 2, 'F'); - } - /** A status pill on baseline `y`; `x` is its right edge when right-aligned. */ - pill(label: string, x: number, y: number, tone: Tone, alignRight = false): void { - const [bg, ink] = PILL[tone]; - const w = this.width(label, 7.5, true) + 4.4; - const left = alignRight ? x - w : x; - this.doc.setFillColor(bg); - this.doc.roundedRect(left, y - 3.6, w, 5, 2.5, 2.5, 'F'); - this.text(label, left + 2.2, y, { size: 7.5, bold: true, color: ink }); - } - /** Figure with a smaller muted unit after it. */ - figure(value: string, unit: string, x: number, y: number, size: number, color = C.text): void { - this.text(value, x, y, { size, bold: true, color }); - this.text(unit, x + this.width(value, size, true) + 1, y, { size: Math.max(7, size * 0.42), color: C.muted }); - } - /** The MeterX mark: a dial with download and upload arrows crossing into an X, centred on (x, y), dial radius r. */ - mark(x: number, y: number, r: number): void { - const doc = this.doc, k = r / 43; - const P = (gx: number, gy: number): [number, number] => [x + (gx - 64) * k, y + (gy - 64) * k]; - const path = (pts: Array<[number, number]>, color: string, w: number) => { - doc.setDrawColor(color).setLineWidth(w * k); - for (let i = 1; i < pts.length; i++) doc.line(...P(...pts[i - 1]), ...P(...pts[i])); - }; - doc.setLineCap('round').setLineJoin('round'); - const from = (135 * Math.PI) / 180, to = (405 * Math.PI) / 180, steps = 28; - path(Array.from({ length: steps + 1 }, (_, i) => { - const a = from + ((to - from) * i) / steps; - return [64 + 43 * Math.cos(a), 64 + 43 * Math.sin(a)] as [number, number]; - }), '#cdd2d8', 8); - path([[46, 46], [78, 78]], C.data, 10); - path([[84, 64], [84, 84], [64, 84]], C.data, 10); - path([[48, 80], [80, 48]], '#ffffff', 20); // the gap where upload crosses download - path([[48, 80], [80, 48]], C.text, 10); - path([[62, 46], [82, 46], [82, 66]], C.text, 10); - } -} - -function header(d: Draw, s: Summary, now: number): number { - d.mark(M + 3.2, 19, 3.2); - d.text('MeterX', M + 9, 18.6, { size: 11, bold: true }); - d.text('Internet speed and quality', M + 9, 22.6, { size: 7.5, color: C.muted }); - d.text(`Generated ${day(now)}, ${new Date(now).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}`, M + W, 18.6, { size: 7.5, color: C.muted, align: 'right' }); - const period = day(s.from) !== day(s.to) ? `${day(s.from)} to ${day(s.to)}` : day(s.to); - d.text(`${period} · ${s.count} test${s.count === 1 ? '' : 's'}`, M + W, 22.6, { size: 7.5, color: C.muted, align: 'right' }); - d.rule(M, 27, M + W, C.border); - return 38; -} - -function intro(d: Draw, s: Summary, plan: IspPlan | null, y: number): number { - d.text('Connection report', M, y, { size: 18, bold: true }); - const parts: string[] = []; - if (s.download) { - let p = `Download averaged ${fmtSpeed(s.download.avg)} Mbps over ${s.count} test${s.count === 1 ? '' : 's'}`; - const share = plan ? planShare(s.download.avg, plan.downloadMbps) : null; - p += share !== null ? `, ${Math.round(share)}% of your ${plan!.downloadMbps} Mbps plan.` : '.'; - parts.push(p); - } - if (s.upload && s.ping) parts.push(`Upload averaged ${fmtSpeed(s.upload.avg)} Mbps and ping was typically ${fmt(s.ping.median, 0)} ms.`); - if (s.trend !== null) { - parts.push(Math.abs(s.trend) < 5 ? 'Download speed held steady across the period.' - : `Download speed ${s.trend > 0 ? 'rose' : 'fell'} ${Math.abs(Math.round(s.trend))}% from the first half of the period to the second.`); - } - d.doc.setFont('helvetica', 'normal').setFontSize(10); - const lines = d.doc.splitTextToSize(parts.join(' '), W * 0.85) as string[]; - lines.forEach((l, i) => d.text(l, M, y + 7.5 + i * 5, { size: 10, color: C.muted })); - return y + 7.5 + (lines.length - 1) * 5 + 7; -} - -function answer(d: Draw, s: Summary, y: number): number { - const h = 32; - d.card(M, y, W, h); - d.text('Download, average', M + 6, y + 8.5, { size: 9, color: C.muted }); - if (s.quality) d.pill(s.quality.label, M + W - 6, y + 8.5, qualityTone(s.quality.cls), true); - d.figure(s.download ? fmtSpeed(s.download.avg) : NONE, 'Mbps', M + 6, y + 22, 30, C.data); - const notes = [range(s.download, fmtSpeed) && `Range ${range(s.download, fmtSpeed)} Mbps`, - s.count > 1 ? `Steadiness ${Math.round(s.steadiness)}%` : ''].filter(Boolean).join(' · '); - if (notes) d.text(notes, M + 6, y + 28, { size: 7.5, color: C.muted }); - return y + h + 4; -} - -function strip(d: Draw, s: Summary, y: number): number { - const h = 22, cw = W / 4; - const cells: Array<[string, Spread | null, 'avg' | 'median', string, (v: number) => string]> = [ - ['Upload, average', s.upload, 'avg', 'Mbps', fmtSpeed], - ['Ping, median', s.ping, 'median', 'ms', v => fmt(v, 0)], - ['Jitter, median', s.jitter, 'median', 'ms', v => fmt(v, 1)], - ['Packet loss, average', s.loss, 'avg', '%', v => fmt(v, 1)], - ]; - d.card(M, y, W, h); - cells.forEach(([label, sp, k, unit, f], i) => { - const x = M + i * cw + 5; - if (i) { d.doc.setDrawColor(C.rule); d.doc.line(M + i * cw, y + 3, M + i * cw, y + h - 3); } - d.text(label, x, y + 6.5, { size: 7.5, color: C.muted }); - d.figure(sp ? f(sp[k]) : NONE, unit, x, y + 14, 15); - const r = range(sp, f); - if (r) d.text(r, x, y + 19, { size: 6.5, color: C.muted }); - }); - return y + h + 4; -} - -function loadAndReady(d: Draw, s: Summary, y: number): number { - const h = 40, w = (W - 4) / 2; - d.card(M, y, w, h); - d.text('Latency under load', M + 5, y + 7.5, { size: 10, bold: true }); - if (s.grade) d.pill(`Grade ${s.grade}`, M + w - 5, y + 7.5, gradeTone(s.grade), true); - const rows: Array<[string, number | undefined, string]> = [ - ['Idle', s.ping?.median, C.text], ['Downloading', s.loadedPingDown?.median, C.second], ['Uploading', s.loadedPingUp?.median, C.second], - ]; - const max = Math.max(1, ...rows.map(r => r[1] ?? 0)); - const bx = M + 27, bw = w - 50; - rows.forEach(([label, v, color], i) => { - const ry = y + 16 + i * 7; - d.text(label, M + 5, ry, { size: 8.5, color: C.muted }); - d.bar(bx, ry - 2, bw, 1.8, C.raised); - if (v !== undefined) d.bar(bx, ry - 2, Math.max(1.8, (bw * v) / max), 1.8, color); - d.text(v === undefined ? NONE : `${fmt(v, 0)} ms`, M + w - 5, ry, { size: 8.5, align: 'right' }); - }); - d.text('Median ping in each phase. A big jump under load means bufferbloat.', M + 5, y + h - 4, { size: 6.5, color: C.muted }); - - const x = M + w + 4; - d.card(x, y, w, h); - d.text('Good enough for', x + 5, y + 7.5, { size: 10, bold: true }); - USE_CASES.forEach((u, i) => { - const ry = y + 16 + i * 6.4; - d.text(u.label, x + 5, ry, { size: 8.5 }); - const score = s.useCases?.[u.key]; - if (score) d.pill(u.words[score], x + w - 5, ry, scoreTone[score], true); - else d.text(NONE, x + w - 5, ry, { size: 8.5, color: C.muted, align: 'right' }); - }); - return y + h + 4; -} - -function planCard(d: Draw, s: Summary, plan: IspPlan, y: number): number { - const rows = ([['Download', s.download?.avg, plan.downloadMbps, C.data], ['Upload', s.upload?.avg, plan.uploadMbps, C.second]] as const) - .filter(r => r[2] > 0); - const h = 10 + rows.length * 11; - d.card(M, y, W, h); - d.text(`Against your plan${plan.providerName ? ` (${plan.providerName})` : ''}`, M + 5, y + 7.5, { size: 10, bold: true }); - rows.forEach(([label, got, planned, color], i) => { - const ry = y + 15 + i * 11; - const share = planShare(got, planned); - d.text(label, M + 5, ry, { size: 8.5, color: C.muted }); - d.text(`${got === undefined ? NONE : fmtSpeed(got)} of ${planned} Mbps${share !== null ? ` · ${Math.round(share)}%` : ''}`, M + W - 5, ry, { size: 8.5, bold: true, align: 'right' }); - const bw = W - 10, scale = Math.max(planned, got ?? 0); - d.bar(M + 5, ry + 2, bw, 2, C.raised); - if (got !== undefined) d.bar(M + 5, ry + 2, Math.max(2, (bw * got) / scale), 2, color); - d.doc.setFillColor(C.text); - d.doc.rect(M + 5 + (bw * planned) / scale - 0.3, ry + 1, 0.6, 4, 'F'); - }); - return y + h + 4; -} - -function chart(d: Draw, hist: HistoryEntry[], y: number): number { - const pts = [...hist].reverse().filter(e => e.downloadSpeed !== undefined); - const h = pts.length < 2 ? 22 : 66; - if (y + h > BOTTOM) { d.doc.addPage(); y = M; } - d.card(M, y, W, h); - d.text('Speed over time', M + 5, y + 7.5, { size: 10, bold: true }); - if (pts.length < 2) { - d.text('Run at least two tests to draw the trend.', M + 5, y + 15, { size: 9, color: C.muted }); - return y + h + 4; - } - let lx = M + W - 5; - for (const [label, color] of [['Upload', C.second], ['Download', C.data]] as const) { - d.text(label, lx, y + 7.5, { size: 7.5, color: C.muted, align: 'right' }); - lx -= d.width(label, 7.5) + 1.5; - d.bar(lx - 4, y + 6.1, 4, 0.9, color); - lx -= 8; - } - const px = M + 15, pw = W - 21, py = y + 15, ph = h - 26; - const top = niceMax(Math.max(...pts.map(e => Math.max(e.downloadSpeed ?? 0, e.uploadSpeed ?? 0)))); - for (let i = 0; i <= 4; i++) { - const gy = py + ph - (ph * i) / 4; - d.rule(px, gy, px + pw, C.grid); - d.text(fmt((top * i) / 4, 0), px - 2, gy + 1, { size: 6.5, color: C.muted, align: 'right' }); - } - d.text('Mbps', px - 2, py - 4, { size: 6.5, color: C.muted, align: 'right' }); - const t0 = pts[0].timestamp, t1 = pts[pts.length - 1].timestamp, span = Math.max(1, t1 - t0); - const X = (t: number) => px + (pw * (t - t0)) / span; - const Y = (v: number) => py + ph - (ph * v) / top; - const line = (k: 'downloadSpeed' | 'uploadSpeed', color: string, width: number) => { - const p = pts.filter(e => e[k] !== undefined); - d.doc.setDrawColor(color); - d.doc.setLineWidth(width); - d.doc.setLineJoin('round'); - for (let i = 1; i < p.length; i++) d.doc.line(X(p[i - 1].timestamp), Y(p[i - 1][k]!), X(p[i].timestamp), Y(p[i][k]!)); - }; - line('uploadSpeed', C.second, 0.45); - line('downloadSpeed', C.data, 0.7); - const sameDay = day(t0) === day(t1); - const label = (t: number) => (sameDay ? new Date(t).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }) - : new Date(t).toLocaleDateString(undefined, { day: 'numeric', month: 'short' })); - d.text(label(t0), px, py + ph + 5, { size: 6.5, color: C.muted }); - d.text(label(t0 + span / 2), px + pw / 2, py + ph + 5, { size: 6.5, color: C.muted, align: 'center' }); - d.text(label(t1), px + pw, py + ph + 5, { size: 6.5, color: C.muted, align: 'right' }); - return y + h + 4; -} - -/** Round an axis maximum up to a value whose quarters are round numbers. */ -export function niceMax(v: number): number { - if (!(v > 0)) return 10; - const p = 10 ** Math.floor(Math.log10(v)); - return ([1, 1.2, 1.6, 2, 2.4, 3, 4, 5, 6, 8, 10].find(m => m * p >= v) ?? 10) * p; -} - -const COLS: Array<{ label: string; w: number; right?: boolean }> = [ - { label: 'When', w: 30 }, { label: 'Down Mbps', w: 21, right: true }, { label: 'Up Mbps', w: 19, right: true }, - { label: 'Ping ms', w: 18, right: true }, { label: 'Jitter ms', w: 19, right: true }, { label: 'Loss %', w: 16, right: true }, - { label: 'Load grade', w: 21, right: true }, { label: 'Quality', w: 34, right: true }, -]; - -function results(d: Draw, hist: HistoryEntry[], y: number): void { - const ROW = 7; - const head = (yy: number, cont: boolean) => { - d.text(cont ? 'Results (continued)' : 'Results', M, yy, { size: 10, bold: true }); - let x = M; - for (const c of COLS) { - d.text(c.label, c.right ? x + c.w : x, yy + 8, { size: 7, color: C.muted, align: c.right ? 'right' : 'left' }); - x += c.w; - } - d.rule(M, yy + 10.5, M + W, C.border); - return yy + 10.5; - }; - y = y + 10.5 + ROW * 3 > BOTTOM ? (d.doc.addPage(), M + 4) : y + 4; - y = head(y, false); - for (const e of hist) { - if (y + ROW > BOTTOM) { d.doc.addPage(); y = head(M + 4, true); } - const q = qualityOf(e); - const cells = [when(e.timestamp), fmtSpeed(e.downloadSpeed), fmtSpeed(e.uploadSpeed), fmt(e.ping, 0), fmt(e.jitter, 1), fmt(e.packetLoss, 1), e.bufferbloatGrade ?? NONE]; - const by = y + ROW - 2.2; - let x = M; - cells.forEach((t, i) => { - const c = COLS[i]; - d.text(t, c.right ? x + c.w : x, by, { size: 8, color: i === 0 ? C.muted : i === 1 ? C.data : C.text, bold: i === 1, align: c.right ? 'right' : 'left' }); - x += c.w; - }); - if (q) d.pill(q.label, M + W, by, qualityTone(q.cls), true); - y += ROW; - d.rule(M, y, M + W); - } -} - -/** Footer and a faint diagonal brand mark on every page. */ -function footers(d: Draw, id: string, faint: unknown, solid: unknown): void { - const n = d.doc.getNumberOfPages(); - const gs = (g: unknown) => d.doc.setGState(g as Parameters[0]); - for (let p = 1; p <= n; p++) { - d.doc.setPage(p); - gs(faint); - d.doc.setFont('helvetica', 'bold').setFontSize(96).setTextColor(C.text); - d.doc.text('MeterX', PAGE_W / 2 - 52, PAGE_H / 2 + 34, { angle: 35 }); - gs(solid); - d.rule(M, FOOT_Y - 4, M + W, C.border); - d.text(`MeterX v${VERSION} · Report ID ${id} · github.com/ajithakdev/meterx`, M, FOOT_Y, { size: 7, color: C.muted }); - d.text(`Page ${p} of ${n}`, M + W, FOOT_Y, { size: 7, color: C.muted, align: 'right' }); - } -} diff --git a/apps/extension/src/lib/format.ts b/apps/extension/src/lib/format.ts index 3bacc60..8408679 100644 --- a/apps/extension/src/lib/format.ts +++ b/apps/extension/src/lib/format.ts @@ -1,43 +1,6 @@ /** - * Formatting helpers. + * Formatting lives in @meterx/shared so the reports and the site use the same rules. * @license AGPL-3.0 */ -/** Shown for a missing value. A plain hyphen: the product never uses em or en dashes. */ -export const NONE = '-'; - -/** Fixed decimals, or NONE for a missing value. */ -export function fmt(v: number | undefined, d: number): string { - return v !== undefined && Number.isFinite(v) ? v.toFixed(d) : NONE; -} - -/** Speeds: one decimal under 100 Mbps, whole numbers above, so the figure stays short. */ -export function fmtSpeed(v: number | undefined): string { - return fmt(v, v !== undefined && v >= 100 ? 0 : 1); -} - -/** YYYY-MM-DD in the browser's own time zone, for file names. */ -export function localDate(d = new Date()): string { - const p = (n: number) => String(n).padStart(2, '0'); - return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; -} - -export function getTimeAgo(date: Date): string { - const mins = Math.floor((Date.now() - date.getTime()) / 60000); - if (mins < 1) return 'just now'; - if (mins < 60) return `${mins}m ago`; - const hours = Math.floor(mins / 60); - if (hours < 24) return `${hours}h ago`; - return `${Math.floor(hours / 24)}d ago`; -} - -/** "Today 14:05", "Yesterday 09:12", "3 Oct 18:40". */ -export function fmtWhen(ts: number, now = Date.now()): string { - const d = new Date(ts); - const time = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); - const day = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime(); - const diff = Math.round((day(new Date(now)) - day(d)) / 86_400_000); - if (diff === 0) return `Today ${time}`; - if (diff === 1) return `Yesterday ${time}`; - return `${d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })} ${time}`; -} +export { NONE, fmt, fmtSpeed, localDate, getTimeAgo, fmtWhen } from '@meterx/shared'; diff --git a/apps/extension/src/lib/isp.ts b/apps/extension/src/lib/isp.ts index 071bdf4..a652254 100644 --- a/apps/extension/src/lib/isp.ts +++ b/apps/extension/src/lib/isp.ts @@ -20,7 +20,5 @@ export async function saveIspPlan(plan: IspPlan | null): Promise { else await chrome.storage.sync.remove(KEY); } -/** Share of the plan delivered, or null when that side of the plan was left blank (#70). */ -export function planShare(measured: number | undefined, planned: number): number | null { - return measured === undefined || !(planned > 0) ? null : (measured / planned) * 100; -} + +export { planShare } from '@meterx/shared'; diff --git a/package-lock.json b/package-lock.json index 97f1fd2..91f08e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,8 +29,8 @@ "name": "@meterx/extension", "license": "AGPL-3.0", "dependencies": { - "@meterx/shared": "*", - "jspdf": "^4.2.1" + "@meterx/reports": "*", + "@meterx/shared": "*" }, "devDependencies": { "@types/chrome": "^0.0.287", @@ -1647,6 +1647,10 @@ "resolved": "apps/extension", "link": true }, + "node_modules/@meterx/reports": { + "resolved": "packages/reports", + "link": true + }, "node_modules/@meterx/server": { "resolved": "services/server", "link": true @@ -8322,6 +8326,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "packages/reports": { + "name": "@meterx/reports", + "license": "AGPL-3.0", + "dependencies": { + "@meterx/shared": "*", + "jspdf": "^4.2.1" + } + }, "packages/shared": { "name": "@meterx/shared", "license": "AGPL-3.0" diff --git a/packages/reports/package.json b/packages/reports/package.json new file mode 100644 index 0000000..5c8df94 --- /dev/null +++ b/packages/reports/package.json @@ -0,0 +1,18 @@ +{ + "name": "@meterx/reports", + "private": true, + "license": "AGPL-3.0", + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "test": "vitest run" + }, + "dependencies": { + "@meterx/shared": "*", + "jspdf": "^4.2.1" + } +} diff --git a/apps/extension/src/lib/export-data.test.ts b/packages/reports/src/data.test.ts similarity index 96% rename from apps/extension/src/lib/export-data.test.ts rename to packages/reports/src/data.test.ts index b329008..48daccf 100644 --- a/apps/extension/src/lib/export-data.test.ts +++ b/packages/reports/src/data.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { escapeCsv } from './export-data'; +import { escapeCsv } from './data'; describe('escapeCsv', () => { it('leaves numbers and plain text alone', () => { @@ -20,7 +20,7 @@ describe('escapeCsv', () => { }); import type { HistoryEntry } from '@meterx/shared'; -import { COLUMNS, toCsv, toJson } from './export-data'; +import { COLUMNS, toCsv, toJson } from './data'; const entry: HistoryEntry = { timestamp: Date.UTC(2026, 9, 24, 8, 30), downloadSpeed: 96.0381, uploadSpeed: 56.31, ping: 202.64, jitter: 13.2, diff --git a/packages/reports/src/data.ts b/packages/reports/src/data.ts new file mode 100644 index 0000000..76c937e --- /dev/null +++ b/packages/reports/src/data.ts @@ -0,0 +1,77 @@ +/** + * CSV and JSON exports. One column list feeds both, so they always carry the same fields. + * @license AGPL-3.0 + */ + +import { VERSION, gradeBufferbloat, localDate, qualityOf, type HistoryEntry } from '@meterx/shared'; +import { summarize, type Spread } from './stats'; + +const round = (v: number | undefined, d: number) => (v === undefined || !Number.isFinite(v) ? undefined : Number(v.toFixed(d))); +const yesNo = (v: boolean | undefined) => (v === undefined ? undefined : v ? 'yes' : 'no'); +const localTime = (ts: number) => { + const d = new Date(ts); + return `${localDate(d)} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; +}; + +type Value = string | number | undefined; + +/** Export columns: JSON key, CSV header, value. Units live in both names. */ +export const COLUMNS: Array<{ key: string; header: string; get: (e: HistoryEntry) => Value }> = [ + { key: 'time', header: 'Time (UTC)', get: e => new Date(e.timestamp).toISOString() }, + { key: 'localTime', header: 'Local time', get: e => localTime(e.timestamp) }, + { key: 'downloadMbps', header: 'Download (Mbps)', get: e => round(e.downloadSpeed, 2) }, + { key: 'uploadMbps', header: 'Upload (Mbps)', get: e => round(e.uploadSpeed, 2) }, + { key: 'pingMs', header: 'Ping (ms)', get: e => round(e.ping, 1) }, + { key: 'jitterMs', header: 'Jitter (ms)', get: e => round(e.jitter, 1) }, + { key: 'packetLossPercent', header: 'Packet loss (%)', get: e => round(e.packetLoss, 1) }, + { key: 'pingWhileDownloadingMs', header: 'Ping while downloading (ms)', get: e => round(e.loadedPingDown, 1) }, + { key: 'pingWhileUploadingMs', header: 'Ping while uploading (ms)', get: e => round(e.loadedPingUp, 1) }, + { key: 'bufferbloatMs', header: 'Bufferbloat (ms)', get: e => round(e.bufferbloatDelta, 1) }, + { key: 'bufferbloatGrade', header: 'Bufferbloat grade', get: e => e.bufferbloatGrade ?? (e.bufferbloatDelta !== undefined ? gradeBufferbloat(e.bufferbloatDelta) : undefined) }, + { key: 'quality', header: 'Quality', get: e => qualityOf(e)?.label }, + { key: 'dnsLookupMs', header: 'DNS lookup (ms)', get: e => round(e.dnsLookupMs, 1) }, + { key: 'ipv4', header: 'IPv4', get: e => yesNo(e.ipv4Ok) }, + { key: 'ipv6', header: 'IPv6', get: e => yesNo(e.ipv6Ok) }, + { key: 'serverLocation', header: 'Server location', get: e => e.edge }, + { key: 'isp', header: 'ISP', get: e => e.isp }, +]; + + +/** Rows newest first, as stored. */ +export function toCsv(history: HistoryEntry[]): string { + const lines = [COLUMNS.map(c => escapeCsv(c.header)).join(',')]; + for (const e of history) lines.push(COLUMNS.map(c => escapeCsv(c.get(e))).join(',')); + return lines.join('\r\n') + '\r\n'; +} + +const typical = (s: Spread | null, k: 'avg' | 'median', d: number) => (s ? round(s[k], d) : undefined); + +export function toJson(history: HistoryEntry[], now = Date.now()): object { + const s = summarize(history); + return { + format: 'meterx-results', + formatVersion: 1, + app: { name: 'MeterX', version: VERSION }, + exportedAt: new Date(now).toISOString(), + period: history.length ? { from: new Date(s.from).toISOString(), to: new Date(s.to).toISOString(), tests: s.count } : null, + summary: { + downloadMbpsAverage: typical(s.download, 'avg', 2), + uploadMbpsAverage: typical(s.upload, 'avg', 2), + pingMsMedian: typical(s.ping, 'median', 1), + jitterMsMedian: typical(s.jitter, 'median', 1), + packetLossPercentAverage: typical(s.loss, 'avg', 1), + bufferbloatGrade: s.grade ?? undefined, + quality: s.quality?.label, + goodEnoughFor: s.useCases ?? undefined, + }, + results: history.map(e => Object.fromEntries(COLUMNS.map(c => [c.key, c.get(e)]).filter(([, v]) => v !== undefined))), + }; +} + +/** RFC 4180 quoting, plus a leading ' on text that a spreadsheet would run as a formula (#78). */ +export function escapeCsv(v: unknown): string { + if (v === null || v === undefined) return ''; + let s = String(v); + if (typeof v === 'string' && /^[=+\-@\t\r]/.test(s)) s = `'${s}`; + return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; +} diff --git a/packages/reports/src/index.ts b/packages/reports/src/index.ts new file mode 100644 index 0000000..b1dbe9d --- /dev/null +++ b/packages/reports/src/index.ts @@ -0,0 +1,9 @@ +/** + * Reports built from MeterX results: the PDF, CSV and JSON exports and their summary stats. + * Pure: no browser-extension APIs, so the extension and the site can both use them. + * @license AGPL-3.0 + */ + +export { buildReport, reportId, niceMax } from './pdf'; +export { COLUMNS, toCsv, toJson, escapeCsv } from './data'; +export { summarize, consistency, trendPercent, type Summary, type Spread } from './stats'; diff --git a/apps/extension/src/lib/export-pdf.test.ts b/packages/reports/src/pdf.test.ts similarity index 94% rename from apps/extension/src/lib/export-pdf.test.ts rename to packages/reports/src/pdf.test.ts index 169ff12..2fddb3c 100644 --- a/apps/extension/src/lib/export-pdf.test.ts +++ b/packages/reports/src/pdf.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import type { HistoryEntry } from '@meterx/shared'; -import { buildReport, niceMax } from './export-pdf'; +import { buildReport, niceMax } from './pdf'; const rows = (n: number): HistoryEntry[] => Array.from({ length: n }, (_, i) => ({ timestamp: Date.UTC(2026, 9, 24) - i * 3_600_000, downloadSpeed: 90 + i, uploadSpeed: 50, ping: 20, jitter: 2, packetLoss: 0, @@ -32,7 +32,7 @@ describe('niceMax', () => { }); }); -import { reportId } from './export-pdf'; +import { reportId } from './pdf'; describe('reportId', () => { it('is stable for the same results and changes when any figure changes', async () => { diff --git a/packages/reports/src/pdf.ts b/packages/reports/src/pdf.ts new file mode 100644 index 0000000..4097b89 --- /dev/null +++ b/packages/reports/src/pdf.ts @@ -0,0 +1,347 @@ +/** + * PDF report, drawn as vector text and shapes with jsPDF (loaded on demand). + * Same design as the app: ink and grey on white, one blue for download, + * status colour only on pills. Colours mirror the light tokens in static/styles/app.css. + * @license AGPL-3.0 + */ + +import { NONE, USE_CASES, VERSION, fmt, fmtSpeed, planShare, qualityOf, type Grade, type HistoryEntry, type IspPlan, type UseCaseScore } from '@meterx/shared'; +import type { jsPDF as JsPdf } from 'jspdf'; +import { summarize, type Spread, type Summary } from './stats'; + +const C = { + text: '#0a0a0a', muted: '#5b6068', border: '#e1e4e8', rule: '#eceef1', raised: '#eaedf1', + data: '#1d5fbf', second: '#8a8f96', grid: '#e6e8eb', +}; +type Tone = 'positive' | 'warning' | 'negative' | 'neutral'; +const PILL: Record = { + positive: ['#dcfae5', '#04250e'], warning: ['#fff4d6', '#7a3a0b'], negative: ['#ffedea', '#831c1e'], neutral: ['#eaedf1', '#3d4148'], +}; +const scoreTone: Record = { good: 'positive', okay: 'warning', poor: 'negative' }; +const gradeTone = (g: Grade): Tone => (g === 'A' || g === 'B' ? 'positive' : g === 'C' ? 'warning' : 'negative'); +const qualityTone = (cls: string): Tone => (cls === 'excellent' || cls === 'good' ? 'positive' : cls === 'fair' ? 'warning' : 'negative'); + +const PAGE_W = 210, PAGE_H = 297, M = 16, W = PAGE_W - 2 * M, FOOT_Y = PAGE_H - 10, BOTTOM = FOOT_Y - 8; + +const day = (ts: number) => new Date(ts).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }); +const when = (ts: number) => `${new Date(ts).toLocaleDateString(undefined, { day: 'numeric', month: 'short' })} ${new Date(ts).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}`; +const range = (s: Spread | null, f: (v: number) => string) => (s && s.n > 1 ? `${f(s.min)} to ${f(s.max)}` : ''); + +/** Builds the report (history newest first, as stored). Split from the download so it can be tested. */ +export async function buildReport(hist: HistoryEntry[], plan: IspPlan | null, now = Date.now()): Promise { + const { jsPDF, GState } = await import('jspdf'); + const doc = new jsPDF({ unit: 'mm', format: 'a4' }); + const id = await reportId(hist); + doc.setProperties({ title: 'MeterX connection report', subject: `Report ID ${id}`, creator: `MeterX ${VERSION}`, keywords: `meterx ${id}` }); + const s = summarize(hist); + const d = new Draw(doc); + + let y = header(d, s, now); + y = intro(d, s, plan, y); + y = answer(d, s, y); + y = strip(d, s, y); + y = loadAndReady(d, s, y); + if (plan && (plan.downloadMbps > 0 || plan.uploadMbps > 0)) y = planCard(d, s, plan, y); + y = chart(d, hist, y); + results(d, hist, y); + footers(d, id, new GState({ opacity: 0.045 }), new GState({ opacity: 1 })); + return doc; +} + +/** + * Short fingerprint of the measurements (time and every figure), shown on each page. + * The same results always give the same ID, so a figure changed in an edited copy no longer matches + * a fresh export. shortcut: not a signature, anyone can recompute it; sign server-side if reports need proof. + */ +export async function reportId(hist: HistoryEntry[]): Promise { + const data = JSON.stringify(hist.map(e => [e.timestamp, e.downloadSpeed, e.uploadSpeed, e.ping, e.jitter, e.packetLoss, e.bufferbloatDelta])); + const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data))); + const hex = [...hash.slice(0, 8)].map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase(); + return hex.match(/.{4}/g)!.join('-'); +} + +/** Thin wrapper so every call sets font, size and colour together. */ +class Draw { + constructor(readonly doc: JsPdf) {} + text(t: string, x: number, y: number, o: { size?: number; bold?: boolean; color?: string; align?: 'left' | 'right' | 'center' } = {}): void { + this.doc.setFont('helvetica', o.bold ? 'bold' : 'normal'); + this.doc.setFontSize(o.size ?? 9); + this.doc.setTextColor(o.color ?? C.text); + this.doc.text(t, x, y, { align: o.align ?? 'left' }); + } + width(t: string, size: number, bold = false): number { + this.doc.setFont('helvetica', bold ? 'bold' : 'normal'); + this.doc.setFontSize(size); + return this.doc.getTextWidth(t); + } + card(x: number, y: number, w: number, h: number): void { + this.doc.setDrawColor(C.border); + this.doc.setLineWidth(0.25); + this.doc.roundedRect(x, y, w, h, 2, 2, 'S'); + } + rule(x1: number, y: number, x2: number, color = C.rule): void { + this.doc.setDrawColor(color); + this.doc.setLineWidth(0.25); + this.doc.line(x1, y, x2, y); + } + bar(x: number, y: number, w: number, h: number, color: string): void { + if (w <= 0) return; + this.doc.setFillColor(color); + this.doc.roundedRect(x, y, w, h, h / 2, h / 2, 'F'); + } + /** A status pill on baseline `y`; `x` is its right edge when right-aligned. */ + pill(label: string, x: number, y: number, tone: Tone, alignRight = false): void { + const [bg, ink] = PILL[tone]; + const w = this.width(label, 7.5, true) + 4.4; + const left = alignRight ? x - w : x; + this.doc.setFillColor(bg); + this.doc.roundedRect(left, y - 3.6, w, 5, 2.5, 2.5, 'F'); + this.text(label, left + 2.2, y, { size: 7.5, bold: true, color: ink }); + } + /** Figure with a smaller muted unit after it. */ + figure(value: string, unit: string, x: number, y: number, size: number, color = C.text): void { + this.text(value, x, y, { size, bold: true, color }); + this.text(unit, x + this.width(value, size, true) + 1, y, { size: Math.max(7, size * 0.42), color: C.muted }); + } + /** The MeterX mark: a dial with download and upload arrows crossing into an X, centred on (x, y), dial radius r. */ + mark(x: number, y: number, r: number): void { + const doc = this.doc, k = r / 43; + const P = (gx: number, gy: number): [number, number] => [x + (gx - 64) * k, y + (gy - 64) * k]; + const path = (pts: Array<[number, number]>, color: string, w: number) => { + doc.setDrawColor(color).setLineWidth(w * k); + for (let i = 1; i < pts.length; i++) doc.line(...P(...pts[i - 1]), ...P(...pts[i])); + }; + doc.setLineCap('round').setLineJoin('round'); + const from = (135 * Math.PI) / 180, to = (405 * Math.PI) / 180, steps = 28; + path(Array.from({ length: steps + 1 }, (_, i) => { + const a = from + ((to - from) * i) / steps; + return [64 + 43 * Math.cos(a), 64 + 43 * Math.sin(a)] as [number, number]; + }), '#cdd2d8', 8); + path([[46, 46], [78, 78]], C.data, 10); + path([[84, 64], [84, 84], [64, 84]], C.data, 10); + path([[48, 80], [80, 48]], '#ffffff', 20); // the gap where upload crosses download + path([[48, 80], [80, 48]], C.text, 10); + path([[62, 46], [82, 46], [82, 66]], C.text, 10); + } +} + +function header(d: Draw, s: Summary, now: number): number { + d.mark(M + 3.2, 19, 3.2); + d.text('MeterX', M + 9, 18.6, { size: 11, bold: true }); + d.text('Internet speed and quality', M + 9, 22.6, { size: 7.5, color: C.muted }); + d.text(`Generated ${day(now)}, ${new Date(now).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}`, M + W, 18.6, { size: 7.5, color: C.muted, align: 'right' }); + const period = day(s.from) !== day(s.to) ? `${day(s.from)} to ${day(s.to)}` : day(s.to); + d.text(`${period} · ${s.count} test${s.count === 1 ? '' : 's'}`, M + W, 22.6, { size: 7.5, color: C.muted, align: 'right' }); + d.rule(M, 27, M + W, C.border); + return 38; +} + +function intro(d: Draw, s: Summary, plan: IspPlan | null, y: number): number { + d.text('Connection report', M, y, { size: 18, bold: true }); + const parts: string[] = []; + if (s.download) { + let p = `Download averaged ${fmtSpeed(s.download.avg)} Mbps over ${s.count} test${s.count === 1 ? '' : 's'}`; + const share = plan ? planShare(s.download.avg, plan.downloadMbps) : null; + p += share !== null ? `, ${Math.round(share)}% of your ${plan!.downloadMbps} Mbps plan.` : '.'; + parts.push(p); + } + if (s.upload && s.ping) parts.push(`Upload averaged ${fmtSpeed(s.upload.avg)} Mbps and ping was typically ${fmt(s.ping.median, 0)} ms.`); + if (s.trend !== null) { + parts.push(Math.abs(s.trend) < 5 ? 'Download speed held steady across the period.' + : `Download speed ${s.trend > 0 ? 'rose' : 'fell'} ${Math.abs(Math.round(s.trend))}% from the first half of the period to the second.`); + } + d.doc.setFont('helvetica', 'normal').setFontSize(10); + const lines = d.doc.splitTextToSize(parts.join(' '), W * 0.85) as string[]; + lines.forEach((l, i) => d.text(l, M, y + 7.5 + i * 5, { size: 10, color: C.muted })); + return y + 7.5 + (lines.length - 1) * 5 + 7; +} + +function answer(d: Draw, s: Summary, y: number): number { + const h = 32; + d.card(M, y, W, h); + d.text('Download, average', M + 6, y + 8.5, { size: 9, color: C.muted }); + if (s.quality) d.pill(s.quality.label, M + W - 6, y + 8.5, qualityTone(s.quality.cls), true); + d.figure(s.download ? fmtSpeed(s.download.avg) : NONE, 'Mbps', M + 6, y + 22, 30, C.data); + const notes = [range(s.download, fmtSpeed) && `Range ${range(s.download, fmtSpeed)} Mbps`, + s.count > 1 ? `Steadiness ${Math.round(s.steadiness)}%` : ''].filter(Boolean).join(' · '); + if (notes) d.text(notes, M + 6, y + 28, { size: 7.5, color: C.muted }); + return y + h + 4; +} + +function strip(d: Draw, s: Summary, y: number): number { + const h = 22, cw = W / 4; + const cells: Array<[string, Spread | null, 'avg' | 'median', string, (v: number) => string]> = [ + ['Upload, average', s.upload, 'avg', 'Mbps', fmtSpeed], + ['Ping, median', s.ping, 'median', 'ms', v => fmt(v, 0)], + ['Jitter, median', s.jitter, 'median', 'ms', v => fmt(v, 1)], + ['Packet loss, average', s.loss, 'avg', '%', v => fmt(v, 1)], + ]; + d.card(M, y, W, h); + cells.forEach(([label, sp, k, unit, f], i) => { + const x = M + i * cw + 5; + if (i) { d.doc.setDrawColor(C.rule); d.doc.line(M + i * cw, y + 3, M + i * cw, y + h - 3); } + d.text(label, x, y + 6.5, { size: 7.5, color: C.muted }); + d.figure(sp ? f(sp[k]) : NONE, unit, x, y + 14, 15); + const r = range(sp, f); + if (r) d.text(r, x, y + 19, { size: 6.5, color: C.muted }); + }); + return y + h + 4; +} + +function loadAndReady(d: Draw, s: Summary, y: number): number { + const h = 40, w = (W - 4) / 2; + d.card(M, y, w, h); + d.text('Latency under load', M + 5, y + 7.5, { size: 10, bold: true }); + if (s.grade) d.pill(`Grade ${s.grade}`, M + w - 5, y + 7.5, gradeTone(s.grade), true); + const rows: Array<[string, number | undefined, string]> = [ + ['Idle', s.ping?.median, C.text], ['Downloading', s.loadedPingDown?.median, C.second], ['Uploading', s.loadedPingUp?.median, C.second], + ]; + const max = Math.max(1, ...rows.map(r => r[1] ?? 0)); + const bx = M + 27, bw = w - 50; + rows.forEach(([label, v, color], i) => { + const ry = y + 16 + i * 7; + d.text(label, M + 5, ry, { size: 8.5, color: C.muted }); + d.bar(bx, ry - 2, bw, 1.8, C.raised); + if (v !== undefined) d.bar(bx, ry - 2, Math.max(1.8, (bw * v) / max), 1.8, color); + d.text(v === undefined ? NONE : `${fmt(v, 0)} ms`, M + w - 5, ry, { size: 8.5, align: 'right' }); + }); + d.text('Median ping in each phase. A big jump under load means bufferbloat.', M + 5, y + h - 4, { size: 6.5, color: C.muted }); + + const x = M + w + 4; + d.card(x, y, w, h); + d.text('Good enough for', x + 5, y + 7.5, { size: 10, bold: true }); + USE_CASES.forEach((u, i) => { + const ry = y + 16 + i * 6.4; + d.text(u.label, x + 5, ry, { size: 8.5 }); + const score = s.useCases?.[u.key]; + if (score) d.pill(u.words[score], x + w - 5, ry, scoreTone[score], true); + else d.text(NONE, x + w - 5, ry, { size: 8.5, color: C.muted, align: 'right' }); + }); + return y + h + 4; +} + +function planCard(d: Draw, s: Summary, plan: IspPlan, y: number): number { + const rows = ([['Download', s.download?.avg, plan.downloadMbps, C.data], ['Upload', s.upload?.avg, plan.uploadMbps, C.second]] as const) + .filter(r => r[2] > 0); + const h = 10 + rows.length * 11; + d.card(M, y, W, h); + d.text(`Against your plan${plan.providerName ? ` (${plan.providerName})` : ''}`, M + 5, y + 7.5, { size: 10, bold: true }); + rows.forEach(([label, got, planned, color], i) => { + const ry = y + 15 + i * 11; + const share = planShare(got, planned); + d.text(label, M + 5, ry, { size: 8.5, color: C.muted }); + d.text(`${got === undefined ? NONE : fmtSpeed(got)} of ${planned} Mbps${share !== null ? ` · ${Math.round(share)}%` : ''}`, M + W - 5, ry, { size: 8.5, bold: true, align: 'right' }); + const bw = W - 10, scale = Math.max(planned, got ?? 0); + d.bar(M + 5, ry + 2, bw, 2, C.raised); + if (got !== undefined) d.bar(M + 5, ry + 2, Math.max(2, (bw * got) / scale), 2, color); + d.doc.setFillColor(C.text); + d.doc.rect(M + 5 + (bw * planned) / scale - 0.3, ry + 1, 0.6, 4, 'F'); + }); + return y + h + 4; +} + +function chart(d: Draw, hist: HistoryEntry[], y: number): number { + const pts = [...hist].reverse().filter(e => e.downloadSpeed !== undefined); + const h = pts.length < 2 ? 22 : 66; + if (y + h > BOTTOM) { d.doc.addPage(); y = M; } + d.card(M, y, W, h); + d.text('Speed over time', M + 5, y + 7.5, { size: 10, bold: true }); + if (pts.length < 2) { + d.text('Run at least two tests to draw the trend.', M + 5, y + 15, { size: 9, color: C.muted }); + return y + h + 4; + } + let lx = M + W - 5; + for (const [label, color] of [['Upload', C.second], ['Download', C.data]] as const) { + d.text(label, lx, y + 7.5, { size: 7.5, color: C.muted, align: 'right' }); + lx -= d.width(label, 7.5) + 1.5; + d.bar(lx - 4, y + 6.1, 4, 0.9, color); + lx -= 8; + } + const px = M + 15, pw = W - 21, py = y + 15, ph = h - 26; + const top = niceMax(Math.max(...pts.map(e => Math.max(e.downloadSpeed ?? 0, e.uploadSpeed ?? 0)))); + for (let i = 0; i <= 4; i++) { + const gy = py + ph - (ph * i) / 4; + d.rule(px, gy, px + pw, C.grid); + d.text(fmt((top * i) / 4, 0), px - 2, gy + 1, { size: 6.5, color: C.muted, align: 'right' }); + } + d.text('Mbps', px - 2, py - 4, { size: 6.5, color: C.muted, align: 'right' }); + const t0 = pts[0].timestamp, t1 = pts[pts.length - 1].timestamp, span = Math.max(1, t1 - t0); + const X = (t: number) => px + (pw * (t - t0)) / span; + const Y = (v: number) => py + ph - (ph * v) / top; + const line = (k: 'downloadSpeed' | 'uploadSpeed', color: string, width: number) => { + const p = pts.filter(e => e[k] !== undefined); + d.doc.setDrawColor(color); + d.doc.setLineWidth(width); + d.doc.setLineJoin('round'); + for (let i = 1; i < p.length; i++) d.doc.line(X(p[i - 1].timestamp), Y(p[i - 1][k]!), X(p[i].timestamp), Y(p[i][k]!)); + }; + line('uploadSpeed', C.second, 0.45); + line('downloadSpeed', C.data, 0.7); + const sameDay = day(t0) === day(t1); + const label = (t: number) => (sameDay ? new Date(t).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }) + : new Date(t).toLocaleDateString(undefined, { day: 'numeric', month: 'short' })); + d.text(label(t0), px, py + ph + 5, { size: 6.5, color: C.muted }); + d.text(label(t0 + span / 2), px + pw / 2, py + ph + 5, { size: 6.5, color: C.muted, align: 'center' }); + d.text(label(t1), px + pw, py + ph + 5, { size: 6.5, color: C.muted, align: 'right' }); + return y + h + 4; +} + +/** Round an axis maximum up to a value whose quarters are round numbers. */ +export function niceMax(v: number): number { + if (!(v > 0)) return 10; + const p = 10 ** Math.floor(Math.log10(v)); + return ([1, 1.2, 1.6, 2, 2.4, 3, 4, 5, 6, 8, 10].find(m => m * p >= v) ?? 10) * p; +} + +const COLS: Array<{ label: string; w: number; right?: boolean }> = [ + { label: 'When', w: 30 }, { label: 'Down Mbps', w: 21, right: true }, { label: 'Up Mbps', w: 19, right: true }, + { label: 'Ping ms', w: 18, right: true }, { label: 'Jitter ms', w: 19, right: true }, { label: 'Loss %', w: 16, right: true }, + { label: 'Load grade', w: 21, right: true }, { label: 'Quality', w: 34, right: true }, +]; + +function results(d: Draw, hist: HistoryEntry[], y: number): void { + const ROW = 7; + const head = (yy: number, cont: boolean) => { + d.text(cont ? 'Results (continued)' : 'Results', M, yy, { size: 10, bold: true }); + let x = M; + for (const c of COLS) { + d.text(c.label, c.right ? x + c.w : x, yy + 8, { size: 7, color: C.muted, align: c.right ? 'right' : 'left' }); + x += c.w; + } + d.rule(M, yy + 10.5, M + W, C.border); + return yy + 10.5; + }; + y = y + 10.5 + ROW * 3 > BOTTOM ? (d.doc.addPage(), M + 4) : y + 4; + y = head(y, false); + for (const e of hist) { + if (y + ROW > BOTTOM) { d.doc.addPage(); y = head(M + 4, true); } + const q = qualityOf(e); + const cells = [when(e.timestamp), fmtSpeed(e.downloadSpeed), fmtSpeed(e.uploadSpeed), fmt(e.ping, 0), fmt(e.jitter, 1), fmt(e.packetLoss, 1), e.bufferbloatGrade ?? NONE]; + const by = y + ROW - 2.2; + let x = M; + cells.forEach((t, i) => { + const c = COLS[i]; + d.text(t, c.right ? x + c.w : x, by, { size: 8, color: i === 0 ? C.muted : i === 1 ? C.data : C.text, bold: i === 1, align: c.right ? 'right' : 'left' }); + x += c.w; + }); + if (q) d.pill(q.label, M + W, by, qualityTone(q.cls), true); + y += ROW; + d.rule(M, y, M + W); + } +} + +/** Footer and a faint diagonal brand mark on every page. */ +function footers(d: Draw, id: string, faint: unknown, solid: unknown): void { + const n = d.doc.getNumberOfPages(); + const gs = (g: unknown) => d.doc.setGState(g as Parameters[0]); + for (let p = 1; p <= n; p++) { + d.doc.setPage(p); + gs(faint); + d.doc.setFont('helvetica', 'bold').setFontSize(96).setTextColor(C.text); + d.doc.text('MeterX', PAGE_W / 2 - 52, PAGE_H / 2 + 34, { angle: 35 }); + gs(solid); + d.rule(M, FOOT_Y - 4, M + W, C.border); + d.text(`MeterX v${VERSION} · Report ID ${id} · github.com/ajithakdev/meterx`, M, FOOT_Y, { size: 7, color: C.muted }); + d.text(`Page ${p} of ${n}`, M + W, FOOT_Y, { size: 7, color: C.muted, align: 'right' }); + } +} diff --git a/apps/extension/src/lib/report-stats.test.ts b/packages/reports/src/stats.test.ts similarity index 94% rename from apps/extension/src/lib/report-stats.test.ts rename to packages/reports/src/stats.test.ts index 601b8ae..ccb6592 100644 --- a/apps/extension/src/lib/report-stats.test.ts +++ b/packages/reports/src/stats.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { consistency, trendPercent } from './report-stats'; +import { consistency, trendPercent } from './stats'; describe('trendPercent', () => { it('has no trend for one result (#76)', () => { @@ -25,7 +25,7 @@ describe('consistency', () => { }); }); -import { summarize } from './report-stats'; +import { summarize } from './stats'; describe('summarize', () => { it('is empty-safe', () => { diff --git a/apps/extension/src/lib/report-stats.ts b/packages/reports/src/stats.ts similarity index 93% rename from apps/extension/src/lib/report-stats.ts rename to packages/reports/src/stats.ts index ed9bf80..d141548 100644 --- a/apps/extension/src/lib/report-stats.ts +++ b/packages/reports/src/stats.ts @@ -3,9 +3,7 @@ * @license AGPL-3.0 */ -import type { Grade, HistoryEntry } from '@meterx/shared'; -import { gradeBufferbloat, median } from '../background/measure'; -import { getQuality, scoreUseCases, type QualityLabel, type UseCaseScores } from './quality'; +import { getQuality, gradeBufferbloat, median, scoreUseCases, type Grade, type HistoryEntry, type QualityLabel, type UseCaseScores } from '@meterx/shared'; const avg = (a: number[]) => (a.length ? a.reduce((s, v) => s + v, 0) / a.length : 0); diff --git a/packages/reports/tsconfig.json b/packages/reports/tsconfig.json new file mode 100644 index 0000000..1a55471 --- /dev/null +++ b/packages/reports/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "declaration": true, + "noEmit": true, + "resolveJsonModule": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/apps/extension/src/lib/format.test.ts b/packages/shared/src/format.test.ts similarity index 100% rename from apps/extension/src/lib/format.test.ts rename to packages/shared/src/format.test.ts diff --git a/packages/shared/src/format.ts b/packages/shared/src/format.ts new file mode 100644 index 0000000..3bacc60 --- /dev/null +++ b/packages/shared/src/format.ts @@ -0,0 +1,43 @@ +/** + * Formatting helpers. + * @license AGPL-3.0 + */ + +/** Shown for a missing value. A plain hyphen: the product never uses em or en dashes. */ +export const NONE = '-'; + +/** Fixed decimals, or NONE for a missing value. */ +export function fmt(v: number | undefined, d: number): string { + return v !== undefined && Number.isFinite(v) ? v.toFixed(d) : NONE; +} + +/** Speeds: one decimal under 100 Mbps, whole numbers above, so the figure stays short. */ +export function fmtSpeed(v: number | undefined): string { + return fmt(v, v !== undefined && v >= 100 ? 0 : 1); +} + +/** YYYY-MM-DD in the browser's own time zone, for file names. */ +export function localDate(d = new Date()): string { + const p = (n: number) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; +} + +export function getTimeAgo(date: Date): string { + const mins = Math.floor((Date.now() - date.getTime()) / 60000); + if (mins < 1) return 'just now'; + if (mins < 60) return `${mins}m ago`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h ago`; + return `${Math.floor(hours / 24)}d ago`; +} + +/** "Today 14:05", "Yesterday 09:12", "3 Oct 18:40". */ +export function fmtWhen(ts: number, now = Date.now()): string { + const d = new Date(ts); + const time = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); + const day = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime(); + const diff = Math.round((day(new Date(now)) - day(d)) / 86_400_000); + if (diff === 0) return `Today ${time}`; + if (diff === 1) return `Yesterday ${time}`; + return `${d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })} ${time}`; +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index d170a1b..df013b6 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -103,4 +103,6 @@ export type BroadcastMessage = | { action: 'testError'; error: string; cancelled?: boolean }; export { SHARE_VIEWER, buildShareUrl, parseShareUrl } from './share'; +export { NONE, fmt, fmtSpeed, localDate, getTimeAgo, fmtWhen } from './format'; +export { median, gradeBufferbloat, planShare } from './stats'; export { getQuality, qualityOf, scoreUseCases, USE_CASES, type QualityLabel, type UseCaseScore, type UseCaseScores } from './quality'; diff --git a/packages/shared/src/stats.ts b/packages/shared/src/stats.ts new file mode 100644 index 0000000..dba7899 --- /dev/null +++ b/packages/shared/src/stats.ts @@ -0,0 +1,27 @@ +/** + * Pure measurement helpers shared by the extension, the reports and the site. + * @license AGPL-3.0 + */ + +import type { Grade } from './index'; + +export function median(values: number[]): number { + if (values.length === 0) return NaN; + const s = [...values].sort((a, b) => a - b); + const mid = s.length >> 1; + return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2; +} + +/** How much latency rises under load. Thresholds follow the Waveform bufferbloat test. */ +export function gradeBufferbloat(deltaMs: number): Grade { + if (deltaMs <= 5) return 'A'; + if (deltaMs <= 30) return 'B'; + if (deltaMs <= 60) return 'C'; + if (deltaMs <= 200) return 'D'; + return 'F'; +} + +/** Share of the plan delivered, or null when that side of the plan was left blank (#70). */ +export function planShare(measured: number | undefined, planned: number): number | null { + return measured === undefined || !(planned > 0) ? null : (measured / planned) * 100; +} From 66ffd88ae3b48c7ed9f618053718263fa1ce2567 Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Sat, 24 Oct 2026 14:53:46 +0530 Subject: [PATCH 19/44] feat(site): multi-page redesign that runs the real extension pages The site now hosts the extension's own popup, side panel and settings in demo mode: a shim stands in for the extension APIs with labelled sample data. On the home page the scroll drives the real popup through a test while a phase log follows it. How it works, Features and Questions are their own pages, the header and footer are shared partials, and the footer is one compact row. The Edge listing link is filled in. --- apps/site/build.mjs | 46 +-- apps/site/src/common.ts | 13 + apps/site/src/demo-shim.ts | 98 ++++++ apps/site/src/home.ts | 208 ++++++------- apps/site/src/quick-test.ts | 48 +++ apps/site/src/sample.test.ts | 26 ++ apps/site/src/sample.ts | 61 ++++ apps/site/static/404.html | 25 +- apps/site/static/_headers | 2 +- apps/site/static/_partials/footer.html | 15 + apps/site/static/_partials/header.html | 13 + apps/site/static/about/index.html | 35 +-- apps/site/static/faq/index.html | 41 +++ apps/site/static/features/index.html | 81 +++++ apps/site/static/get/index.html | 25 +- apps/site/static/how-it-works/index.html | 74 +++++ apps/site/static/img/shot-history-dark.png | Bin 59522 -> 0 bytes apps/site/static/img/shot-history-light.png | Bin 56986 -> 0 bytes apps/site/static/img/shot-load-dark.png | Bin 23727 -> 0 bytes apps/site/static/img/shot-load-light.png | Bin 22615 -> 0 bytes apps/site/static/img/shot-plan-dark.png | Bin 26709 -> 0 bytes apps/site/static/img/shot-plan-light.png | Bin 24343 -> 0 bytes apps/site/static/img/shot-popup-dark.png | Bin 112393 -> 0 bytes apps/site/static/img/shot-popup-light.png | Bin 105289 -> 0 bytes apps/site/static/img/shot-settings-dark.png | Bin 54536 -> 0 bytes apps/site/static/img/shot-settings-light.png | Bin 51834 -> 0 bytes apps/site/static/index.html | 169 ++++------- apps/site/static/privacy/index.html | 25 +- apps/site/static/r/index.html | 27 +- apps/site/static/styles/home.css | 47 +++ apps/site/static/styles/site.css | 296 +++++++++---------- 31 files changed, 838 insertions(+), 537 deletions(-) create mode 100644 apps/site/src/demo-shim.ts create mode 100644 apps/site/src/quick-test.ts create mode 100644 apps/site/src/sample.test.ts create mode 100644 apps/site/src/sample.ts create mode 100644 apps/site/static/_partials/footer.html create mode 100644 apps/site/static/_partials/header.html create mode 100644 apps/site/static/faq/index.html create mode 100644 apps/site/static/features/index.html create mode 100644 apps/site/static/how-it-works/index.html delete mode 100644 apps/site/static/img/shot-history-dark.png delete mode 100644 apps/site/static/img/shot-history-light.png delete mode 100644 apps/site/static/img/shot-load-dark.png delete mode 100644 apps/site/static/img/shot-load-light.png delete mode 100644 apps/site/static/img/shot-plan-dark.png delete mode 100644 apps/site/static/img/shot-plan-light.png delete mode 100644 apps/site/static/img/shot-popup-dark.png delete mode 100644 apps/site/static/img/shot-popup-light.png delete mode 100644 apps/site/static/img/shot-settings-dark.png delete mode 100644 apps/site/static/img/shot-settings-light.png create mode 100644 apps/site/static/styles/home.css diff --git a/apps/site/build.mjs b/apps/site/build.mjs index 465cbe4..ae9bbf1 100644 --- a/apps/site/build.mjs +++ b/apps/site/build.mjs @@ -1,28 +1,40 @@ import { build } from 'esbuild'; -import { cpSync, mkdirSync, rmSync } from 'fs'; -import { dirname, resolve } from 'path'; +import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs'; +import { dirname, join, resolve } from 'path'; import { fileURLToPath } from 'url'; const here = dirname(fileURLToPath(import.meta.url)); const dist = resolve(here, 'dist'); -const ext = resolve(here, '../extension/static'); +const ext = resolve(here, '../extension'); + +if (!existsSync(resolve(ext, 'dist/popup.html'))) throw new Error('Build the extension first: npm run build -w @meterx/extension'); rmSync(dist, { recursive: true, force: true }); mkdirSync(dist, { recursive: true }); -// Pages, styles and images, then the extension's own font and stylesheet so interface fragments look exactly like the app. -cpSync(resolve(here, 'static'), dist, { recursive: true }); -cpSync(resolve(ext, 'fonts'), resolve(dist, 'fonts'), { recursive: true }); -cpSync(resolve(ext, 'styles/app.css'), resolve(dist, 'styles/app.css')); - -await build({ - entryPoints: { home: 'src/home.ts', share: 'src/share.ts', get: 'src/get.ts', common: 'src/common.ts' }, - absWorkingDir: here, - bundle: true, - format: 'esm', - target: 'es2022', - minify: true, - outdir: resolve(dist, 'js'), -}); +// Pages and images; partials are filled in below, not served. +cpSync(resolve(here, 'static'), dist, { recursive: true, filter: src => !src.includes('_partials') }); +cpSync(resolve(ext, 'static/fonts'), resolve(dist, 'fonts'), { recursive: true }); +cpSync(resolve(ext, 'static/styles/app.css'), resolve(dist, 'styles/app.css')); + +// The real extension pages, run in demo mode by the shim, which must load before each page's own script. +const app = resolve(dist, 'app'); +cpSync(resolve(ext, 'dist'), app, { recursive: true, filter: src => !/background\.js$|manifest\.json$/.test(src) }); +for (const page of ['popup.html', 'sidepanel.html', 'options.html']) { + const f = resolve(app, page); + writeFileSync(f, readFileSync(f, 'utf8').replace('\n readFileSync(resolve(here, 'static/_partials', `${name}.html`), 'utf8').trim(); +const header = partial('header'), footer = partial('footer'); +const walk = dir => readdirSync(dir).flatMap(n => { const p = join(dir, n); return statSync(p).isDirectory() ? (p === app ? [] : walk(p)) : [p]; }); +for (const f of walk(dist).filter(p => p.endsWith('.html'))) { + writeFileSync(f, readFileSync(f, 'utf8').replace('', header).replace('', footer)); +} + +const common = { absWorkingDir: here, bundle: true, target: 'es2022', minify: true }; +await build({ ...common, entryPoints: { home: 'src/home.ts', share: 'src/share.ts', get: 'src/get.ts', common: 'src/common.ts' }, format: 'esm', outdir: resolve(dist, 'js') }); +await build({ ...common, entryPoints: { 'demo-shim': 'src/demo-shim.ts' }, format: 'iife', outdir: resolve(dist, 'js') }); console.log('Site built to dist/'); diff --git a/apps/site/src/common.ts b/apps/site/src/common.ts index 5321add..da4b175 100644 --- a/apps/site/src/common.ts +++ b/apps/site/src/common.ts @@ -28,7 +28,20 @@ function entrances(): void { document.querySelectorAll('[data-enter]').forEach(s => io.observe(s)); } +/** Marks the current page in the header and gives the header an edge once the page scrolls under it. */ +function chrome(): void { + const path = location.pathname; + document.querySelectorAll('.links a').forEach(a => { + if (path.startsWith(a.getAttribute('href')!)) a.setAttribute('aria-current', 'page'); + }); + const bar = document.querySelector('.site-bar'); + const onScroll = () => bar?.classList.toggle('is-scrolled', scrollY > 8); + addEventListener('scroll', onScroll, { passive: true }); + onScroll(); +} + wireInstall(); entrances(); +chrome(); const year = document.getElementById('year'); if (year) year.textContent = String(new Date().getFullYear()); diff --git a/apps/site/src/demo-shim.ts b/apps/site/src/demo-shim.ts new file mode 100644 index 0000000..5202e7b --- /dev/null +++ b/apps/site/src/demo-shim.ts @@ -0,0 +1,98 @@ +/** + * Runs the real extension pages (popup, side panel, settings) on the website in demo mode. + * Loaded before the page script; stands in for the extension APIs with sample data in memory. + * The parent page can drive the sample test with postMessage({ mxDemo: 'drive', p }). + * @license AGPL-3.0 + */ + +import type { BroadcastMessage, HistoryEntry } from '@meterx/shared'; +import { SAMPLE_PLAN, sampleFrame, sampleHistory, sampleResult } from './sample'; + +type Area = 'local' | 'sync'; +type Changes = Record; +const data: Record> = { local: { history: sampleHistory() }, sync: { ispPlan: SAMPLE_PLAN } }; +const changeListeners: Array<(c: Changes, area: Area) => void> = []; +const messageListeners: Array<(m: BroadcastMessage) => void> = []; + +function area(name: Area) { + const store = data[name]; + return { + async get(keys?: string | string[] | Record | null) { + if (keys == null) return { ...store }; + if (typeof keys === 'string') return keys in store ? { [keys]: store[keys] } : {}; + if (Array.isArray(keys)) return Object.fromEntries(keys.filter(k => k in store).map(k => [k, store[k]])); + return Object.fromEntries(Object.entries(keys).map(([k, d]) => [k, k in store ? store[k] : d])); + }, + async set(items: Record) { + const changes: Changes = {}; + for (const [k, v] of Object.entries(items)) { changes[k] = { oldValue: store[k], newValue: v }; store[k] = v; } + changeListeners.forEach(f => f(changes, name)); + }, + async remove(keys: string | string[]) { + const changes: Changes = {}; + for (const k of ([] as string[]).concat(keys)) { changes[k] = { oldValue: store[k] }; delete store[k]; } + changeListeners.forEach(f => f(changes, name)); + }, + }; +} + +const broadcast = (m: BroadcastMessage) => messageListeners.forEach(f => f(m)); + +// --- The sample test, played on a timer (Run test) or driven by the parent page's scroll. +let running = false; +let progress = {}; +let timer: number | undefined; + +function show(p: number, save: boolean): void { + const f = sampleFrame(p); + if (f === null) { + if (running) { running = false; broadcast({ action: 'testError', error: 'Test cancelled', cancelled: true }); } + } else if (f === 'done') { + if (running) { + running = false; + const entry = sampleResult(); + if (save) void area('local').set({ history: [entry, ...(data.local.history as HistoryEntry[])].slice(0, 200) }); + broadcast({ action: 'testComplete', data: entry }); + } + } else { + running = true; + progress = f; + broadcast({ action: 'testProgress', data: f }); + } +} + +function play(): void { + clearInterval(timer); + const t0 = performance.now(), length = 7000; // the real test takes about 20 s; the demo plays it faster + timer = window.setInterval(() => { + const p = (performance.now() - t0) / length; + show(Math.min(p, 1), true); + if (p >= 1) clearInterval(timer); + }, 120); +} + +const chromeDemo = { + storage: { local: area('local'), sync: area('sync'), onChanged: { addListener: (f: (c: Changes, a: Area) => void) => changeListeners.push(f) } }, + runtime: { + async sendMessage(m: { action?: string }) { + if (m?.action === 'getState') return running ? { running, progress } : null; + if (m?.action === 'startTest') { play(); return { ok: true }; } + if (m?.action === 'cancelTest') { clearInterval(timer); show(0, false); return { ok: true }; } + return { ok: true }; + }, + onMessage: { addListener: (f: (m: BroadcastMessage) => void) => messageListeners.push(f) }, + getURL: (p: string) => p, + openOptionsPage: () => parent.postMessage({ mxDemo: 'openOptions' }, location.origin), + }, + permissions: { contains: async () => true, request: async () => true }, + sidePanel: { open: async () => parent.postMessage({ mxDemo: 'openSidePanel' }, location.origin) }, + windows: { getCurrent: async () => ({ id: 1 }) }, + tabs: { create: (o: { url?: string }) => { if (o?.url) window.open(o.url, '_blank', 'noopener'); } }, +}; +(window as unknown as { chrome: unknown }).chrome = chromeDemo; + +window.addEventListener('message', e => { + if (e.origin !== location.origin || e.data?.mxDemo !== 'drive' || typeof e.data.p !== 'number') return; + clearInterval(timer); + show(e.data.p, false); +}); diff --git a/apps/site/src/home.ts b/apps/site/src/home.ts index 4d3b76c..2ac67d8 100644 --- a/apps/site/src/home.ts +++ b/apps/site/src/home.ts @@ -1,154 +1,114 @@ /** - * Home page: the in-page quick test, the speed explorer, the pinned test phases and the cycling browser names. + * Home page: scroll drives the real popup through a sample test, the phase log follows it, + * and the closing question measures what a speed buys. * @license AGPL-3.0 */ +import { fmtSpeed } from '@meterx/shared'; import { capacity, formatDuration } from './capacity'; +import { quickTest } from './quick-test'; +import { PHASES, sampleFrame, sampleResult } from './sample'; -const WORKER = 'https://meterx-speedtest.meterx-ajithakdev.workers.dev'; const $ = (id: string) => document.getElementById(id) as T | null; -const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; -const fmtSpeed = (v: number) => (v >= 100 ? v.toFixed(0) : v.toFixed(1)); - -// --- Quick test: a few pings and one download stream for about five seconds. Nothing runs until the visitor asks. - -async function pingMs(): Promise { - const t = performance.now(); - const r = await fetch(`${WORKER}/ping`, { cache: 'no-store' }); - await r.arrayBuffer(); - return performance.now() - t; -} - -async function quickTest(onSpeed: (mbps: number) => void): Promise<{ mbps: number; ping: number }> { - await pingMs(); // warm the connection; the first request includes the TLS handshake - const pings: number[] = []; - for (let i = 0; i < 5; i++) pings.push(await pingMs()); - pings.sort((a, b) => a - b); - const ping = pings[2]; - - const ctrl = new AbortController(); - const stop = setTimeout(() => ctrl.abort(), 5500); - const res = await fetch(`${WORKER}/test-file/25MB.bin`, { cache: 'no-store', signal: ctrl.signal }); - const reader = res.body!.getReader(); - const t0 = performance.now(); - const RAMP = 600; // skip slow start so the figure reflects the steady rate - let bytes = 0, rampBytes = 0, mbps = 0, last = 0; - try { - for (;;) { - const { done, value } = await reader.read(); - if (done) break; - bytes += value.byteLength; - const t = performance.now() - t0; - if (t < RAMP) { rampBytes = bytes; continue; } - mbps = ((bytes - rampBytes) * 8) / ((t - RAMP) * 1000); - if (t - last > 200) { onSpeed(mbps); last = t; } - } - } catch (e) { - if (!(e instanceof DOMException && e.name === 'AbortError')) throw e; - } finally { - clearTimeout(stop); - } - // A fast line can finish inside the ramp window; then the whole transfer is the best estimate. - if (!mbps && bytes) mbps = (bytes * 8) / ((performance.now() - t0) * 1000); - return { mbps, ping }; +// --- Hero: the act's scroll progress is the test's playhead. + +const ORDER = ['connect', 'latency', 'download', 'upload', 'done'] as const; + +function logState(p: number): void { + const f = sampleFrame(p); + const done = f === 'done', r = sampleResult(); + const current = f === null ? null : done ? 'done' : f.phase; + const reached = current ? ORDER.indexOf(current as typeof ORDER[number]) : -1; + const val: Record = { + connect: reached >= 0 ? 'Cloudflare MAA' : '', + latency: reached >= 1 ? `${(done ? r.ping! : (f as { ping?: number })?.ping ?? r.ping!).toFixed(0)} ms` : '', + download: reached >= 2 ? `${fmtSpeed(done || reached > 2 ? r.downloadSpeed : (f as { downloadSpeed?: number }).downloadSpeed)} Mbps` : '', + upload: reached >= 3 ? `${fmtSpeed(done ? r.uploadSpeed : (f as { uploadSpeed?: number }).uploadSpeed)} Mbps` : '', + done: done ? 'Excellent' : '', + }; + document.querySelectorAll('#phaseLog li').forEach((li, i) => { + li.dataset.state = i < reached || (done && i === reached) ? 'done' : i === reached ? 'active' : 'pending'; + li.querySelector('b')!.textContent = val[li.dataset.phase!]; + }); } -function wireQuickTest(): void { - const btn = $('qtRun'), note = $('qtNote'); - if (!btn || !note) return; - btn.addEventListener('click', async () => { - btn.disabled = true; - btn.textContent = 'Testing…'; - note.textContent = 'Measuring ping, then download…'; - try { - const r = await quickTest(m => { note.innerHTML = `Download ${fmtSpeed(m)} Mbps so far…`; }); - note.innerHTML = `Download ${fmtSpeed(r.mbps)} Mbps, ping ${r.ping.toFixed(0)} ms from this page.`; - setExplorer(Math.round(r.mbps)); - } catch { - note.textContent = 'The test server could not be reached from here. Install MeterX to test from your toolbar.'; - } finally { - btn.disabled = false; - btn.textContent = 'Run it again'; +function wireHero(): void { + const act = $('hero'), frame = $('heroPopup'); + if (!act || !frame) return; + let ready = false, last = -1; + frame.addEventListener('load', () => { ready = true; last = -1; }); + const tick = () => { + const p = parseFloat(act.style.getPropertyValue('--sc-p')) || 0; + if (Math.abs(p - last) > 0.002) { + last = p; + logState(p); + // shortcut: the act's progress maps straight onto the test; past PHASES.done the result holds + if (ready) frame.contentWindow?.postMessage({ mxDemo: 'drive', p }, location.origin); } - }); + requestAnimationFrame(tick); + }; + requestAnimationFrame(tick); + logState(0); } -// --- Speed explorer: type a speed, see what it buys. - -function renderExplorer(raw: string): void { - const out = $('explain'); - if (!out) return; - const v = Number(raw.replace(/[^\d.]/g, '')); - const c = capacity(v); - if (!c) { out.innerHTML = '
    • Type a download speed in Mbps
    • '; return; } - out.innerHTML = [ - ['4K streams at once', String(c.streams4k)], - ['HD streams at once', String(c.streamsHd)], - ['A 50 GB game download', formatDuration(c.gameSeconds)], - ['A 4 GB film download', formatDuration(c.filmSeconds)], - ].map(([k, val]) => `
    • ${k}${val}
    • `).join(''); -} +// --- The closing question: type a speed, see what it buys; or measure this browser. -/** The input is part of the headline, so it is exactly as wide as its digits. */ let ruler: CanvasRenderingContext2D | null = null; +/** The input is part of the heading, so it is exactly as wide as its digits. */ function fitInput(input: HTMLInputElement): void { ruler ??= document.createElement('canvas').getContext('2d'); if (!ruler) return; const cs = getComputedStyle(input); ruler.font = `${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`; ruler.letterSpacing = cs.letterSpacing; - input.style.width = `${Math.ceil(ruler.measureText(input.value || '0').width) + 4}px`; + input.style.width = `${Math.ceil(ruler.measureText(input.value || '0').width) + 6}px`; } -function setExplorer(v: number): void { - const input = $('speedInput'); - if (!input) return; - input.value = String(v); +function renderExplorer(input: HTMLInputElement): void { fitInput(input); - renderExplorer(input.value); + const out = $('explain'); + if (!out) return; + const c = capacity(Number(input.value.replace(/[^\d.]/g, ''))); + out.innerHTML = c + ? [['4K streams at once', String(c.streams4k)], ['HD streams at once', String(c.streamsHd)], ['A 50 GB game download', formatDuration(c.gameSeconds)], ['A 4 GB film download', formatDuration(c.filmSeconds)]] + .map(([k, v]) => `
    • ${k}${v}
    • `).join('') + : '
    • Type a download speed in Mbps
    • '; } -function wireExplorer(): void { - const input = $('speedInput'); +function wireClose(): void { + const input = $('speedInput'), btn = $('qtRun'), note = $('qtNote'); if (!input) return; - input.addEventListener('input', () => { fitInput(input); renderExplorer(input.value); }); - document.querySelectorAll('[data-speed]').forEach(b => b.addEventListener('click', () => setExplorer(Number(b.dataset.speed)))); - fitInput(input); - renderExplorer(input.value); -} - -// --- How it works: the bar's segment for the step on screen is lit. - -function wireSteps(): void { - const steps = document.querySelectorAll('[data-step]'); - const segs = document.querySelectorAll('.phase-bar [data-seg]'); - if (!steps.length || !('IntersectionObserver' in window)) return; - const io = new IntersectionObserver(entries => { - for (const e of entries) { - if (!e.isIntersecting) continue; - const id = (e.target as HTMLElement).dataset.step; - segs.forEach(s => s.classList.toggle('on', s.dataset.seg === id)); - steps.forEach(s => s.classList.toggle('on', s === e.target)); + input.addEventListener('input', () => renderExplorer(input)); + document.fonts.ready.then(() => renderExplorer(input)); + renderExplorer(input); + btn?.addEventListener('click', async () => { + if (!note) return; + btn.disabled = true; + btn.textContent = 'Measuring…'; + note.textContent = 'Measuring ping, then download…'; + try { + const r = await quickTest(m => { note.innerHTML = `Download ${fmtSpeed(m)} Mbps so far…`; }); + note.innerHTML = `This browser: download ${fmtSpeed(r.mbps)} Mbps, ping ${r.ping.toFixed(0)} ms.`; + input.value = String(Math.round(r.mbps)); + renderExplorer(input); + } catch { + note.textContent = 'The test server could not be reached from here. Install MeterX to test from your toolbar.'; + } finally { + btn.disabled = false; + btn.textContent = 'Measure again'; } - }, { rootMargin: '-45% 0px -45% 0px' }); - steps.forEach(s => io.observe(s)); -} - -// --- Browser names: one lit at a time. - -function wireNames(): void { - const names = [...document.querySelectorAll('.names .name')]; - if (names.length < 2 || reduced) return; - let i = 0; - setInterval(() => { - names[i].classList.remove('lit'); - i = (i + 1) % names.length; - names[i].classList.add('lit'); - }, 2000); + }); } -wireQuickTest(); -wireExplorer(); -wireSteps(); -wireNames(); +// Buttons inside the demo pages that would open other extension pages go to the matching site page instead. +window.addEventListener('message', e => { + if (e.origin !== location.origin) return; + if (e.data?.mxDemo === 'openOptions') location.href = '/features/#settings'; + if (e.data?.mxDemo === 'openSidePanel') location.href = '/features/#history'; +}); + +// The scroll engine is a classic script loaded before this module; starting it here keeps the page free of inline script (CSP). +(window as unknown as { ScrollCraft?: { mount(root: HTMLElement): void } }).ScrollCraft?.mount(document.body); +wireHero(); +wireClose(); diff --git a/apps/site/src/quick-test.ts b/apps/site/src/quick-test.ts new file mode 100644 index 0000000..c2fa282 --- /dev/null +++ b/apps/site/src/quick-test.ts @@ -0,0 +1,48 @@ +/** + * A short in-page check against the MeterX worker: a few pings and one download stream for about five seconds. + * Nothing runs until the visitor asks. + * @license AGPL-3.0 + */ + +const WORKER = 'https://meterx-speedtest.meterx-ajithakdev.workers.dev'; + +async function pingMs(): Promise { + const t = performance.now(); + const r = await fetch(`${WORKER}/ping`, { cache: 'no-store' }); + await r.arrayBuffer(); + return performance.now() - t; +} + +export async function quickTest(onSpeed: (mbps: number) => void): Promise<{ mbps: number; ping: number }> { + await pingMs(); // warm the connection; the first request includes the TLS handshake + const pings: number[] = []; + for (let i = 0; i < 5; i++) pings.push(await pingMs()); + pings.sort((a, b) => a - b); + const ping = pings[2]; + + const ctrl = new AbortController(); + const stop = setTimeout(() => ctrl.abort(), 5500); + const res = await fetch(`${WORKER}/test-file/25MB.bin`, { cache: 'no-store', signal: ctrl.signal }); + const reader = res.body!.getReader(); + const t0 = performance.now(); + const RAMP = 600; // skip slow start so the figure reflects the steady rate + let bytes = 0, rampBytes = 0, mbps = 0, last = 0; + try { + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + bytes += value.byteLength; + const t = performance.now() - t0; + if (t < RAMP) { rampBytes = bytes; continue; } + mbps = ((bytes - rampBytes) * 8) / ((t - RAMP) * 1000); + if (t - last > 200) { onSpeed(mbps); last = t; } + } + } catch (e) { + if (!(e instanceof DOMException && e.name === 'AbortError')) throw e; + } finally { + clearTimeout(stop); + } + // A fast line can finish inside the ramp window; then the whole transfer is the best estimate. + if (!mbps && bytes) mbps = (bytes * 8) / ((performance.now() - t0) * 1000); + return { mbps, ping }; +} diff --git a/apps/site/src/sample.test.ts b/apps/site/src/sample.test.ts new file mode 100644 index 0000000..863d9d3 --- /dev/null +++ b/apps/site/src/sample.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; +import { PHASES, sampleFrame, sampleHistory, sampleResult } from './sample'; + +describe('sampleFrame', () => { + it('walks the real phase order and ends on done', () => { + expect(sampleFrame(0)).toBeNull(); + const phases = [0.05, 0.1, 0.3, 0.7].map(p => (sampleFrame(p) as { phase: string }).phase); + expect(phases).toEqual(['connect', 'latency', 'download', 'upload']); + expect(sampleFrame(PHASES.done)).toBe('done'); + }); + it('never shows a figure above the final result by more than the wobble', () => { + for (let p = 0.2; p < 0.9; p += 0.01) { + const f = sampleFrame(p) as { downloadSpeed?: number }; + if (f.downloadSpeed) expect(f.downloadSpeed).toBeLessThan(sampleResult().downloadSpeed! * 1.04); + } + }); +}); + +describe('sampleHistory', () => { + it('is stable, newest first, with consistent grades', () => { + const h = sampleHistory(1e12); + expect(h).toHaveLength(30); + expect(h[0].timestamp).toBeGreaterThan(h[1].timestamp); + expect(sampleHistory(1e12)).toEqual(h); + }); +}); diff --git a/apps/site/src/sample.ts b/apps/site/src/sample.ts new file mode 100644 index 0000000..df97641 --- /dev/null +++ b/apps/site/src/sample.ts @@ -0,0 +1,61 @@ +/** + * Sample data for the demo pages: a week of results on a 300/100 plan and one test played frame by frame. + * Everything here is labelled as sample data wherever it is shown. + * @license AGPL-3.0 + */ + +import { gradeBufferbloat, type HistoryEntry, type IspPlan, type TestProgress } from '@meterx/shared'; + +export const SAMPLE_PLAN: IspPlan = { downloadMbps: 300, uploadMbps: 100, providerName: 'Sample ISP' }; + +/** Thirty results, newest first, one every 5.6 hours, with one slow evening. Deterministic. */ +export function sampleHistory(now = Date.now()): HistoryEntry[] { + let seed = 11; + const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647; + return Array.from({ length: 30 }, (_, i) => { + const ping = 18 + rnd() * 8; + const delta = 30 + rnd() * 20; + return { + timestamp: now - (i + 1) * 5.6 * 3600e3, + downloadSpeed: 262 + rnd() * 40 - (i === 9 ? 120 : 0), + uploadSpeed: 88 + rnd() * 10, + ping, jitter: 1.5 + rnd() * 2.5, packetLoss: 0, + bufferbloatDelta: delta, bufferbloatGrade: gradeBufferbloat(delta), + loadedPingDown: ping + delta, loadedPingUp: ping + delta * 0.6, + ipv4Ok: true, ipv6Ok: true, edge: 'MAA', isp: 'Sample ISP', + }; + }); +} + +const FINAL = { downloadSpeed: 284.6, uploadSpeed: 92.1, ping: 19, jitter: 2.1, packetLoss: 0, loadedPingDown: 61, loadedPingUp: 46 }; + +/** The result the sample test ends on. */ +export function sampleResult(now = Date.now()): HistoryEntry { + const delta = Math.max(FINAL.loadedPingDown, FINAL.loadedPingUp) - FINAL.ping; + return { ...FINAL, timestamp: now, bufferbloatDelta: delta, bufferbloatGrade: gradeBufferbloat(delta), ipv4Ok: true, ipv6Ok: true, edge: 'MAA', isp: 'Sample ISP' }; +} + +const clamp = (v: number) => Math.min(1, Math.max(0, v)); +const rise = (t: number) => 1 - Math.exp(-5 * t); +const wobble = (t: number) => 1 + 0.03 * Math.sin(t * 37); + +/** Phase boundaries as shares of the whole test, matching the real timings (about 1 s, 2 s, 9 s, 9 s). */ +export const PHASES = { start: 0.04, latency: 0.08, download: 0.2, upload: 0.56, done: 0.9 } as const; + +/** + * The sample test at position p (0 to 1): null before it starts, 'done' once it has finished, + * otherwise the progress message the real service worker would send at that moment. + */ +export function sampleFrame(p: number): TestProgress | null | 'done' { + if (p < PHASES.start) return null; + if (p >= PHASES.done) return 'done'; + if (p < PHASES.latency) return { phase: 'connect', status: 'Connecting' }; + const lt = clamp((p - PHASES.latency) / (PHASES.download - PHASES.latency)); + const ping = FINAL.ping + 9 * (1 - lt), jitter = FINAL.jitter + 1.5 * (1 - lt); + if (p < PHASES.download) return { phase: 'latency', status: 'Measuring ping', ping, jitter, packetLoss: 0 }; + const dt = clamp((p - PHASES.download) / (PHASES.upload - PHASES.download)); + const down = FINAL.downloadSpeed * rise(dt) * wobble(dt); + if (p < PHASES.upload) return { phase: 'download', status: 'Measuring download', ping, jitter, packetLoss: 0, downloadSpeed: down }; + const ut = clamp((p - PHASES.upload) / (PHASES.done - PHASES.upload)); + return { phase: 'upload', status: 'Measuring upload', ping, jitter, packetLoss: 0, downloadSpeed: FINAL.downloadSpeed, uploadSpeed: FINAL.uploadSpeed * rise(ut) * wobble(ut) }; +} diff --git a/apps/site/static/404.html b/apps/site/static/404.html index 9b772cd..1cff7f4 100644 --- a/apps/site/static/404.html +++ b/apps/site/static/404.html @@ -18,31 +18,12 @@ - - +
      -

      This page does not exist

      The link may be old or mistyped.

      +

      This page does not exist

      The link may be old or mistyped.

      - - + diff --git a/apps/site/static/_headers b/apps/site/static/_headers index 0e258c0..407953f 100644 --- a/apps/site/static/_headers +++ b/apps/site/static/_headers @@ -2,7 +2,7 @@ X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=() - Content-Security-Policy: default-src 'self'; connect-src 'self' https://meterx-speedtest.meterx-ajithakdev.workers.dev; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none' + Content-Security-Policy: default-src 'self'; connect-src 'self' https://meterx-speedtest.meterx-ajithakdev.workers.dev; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; frame-ancestors 'self'; base-uri 'none'; form-action 'none' Strict-Transport-Security: max-age=31536000 /fonts/* diff --git a/apps/site/static/_partials/footer.html b/apps/site/static/_partials/footer.html new file mode 100644 index 0000000..1552111 --- /dev/null +++ b/apps/site/static/_partials/footer.html @@ -0,0 +1,15 @@ + + diff --git a/apps/site/static/_partials/header.html b/apps/site/static/_partials/header.html new file mode 100644 index 0000000..76927d0 --- /dev/null +++ b/apps/site/static/_partials/header.html @@ -0,0 +1,13 @@ + +
      + +
      diff --git a/apps/site/static/about/index.html b/apps/site/static/about/index.html index 55dbded..f8222cf 100644 --- a/apps/site/static/about/index.html +++ b/apps/site/static/about/index.html @@ -18,44 +18,19 @@ - - +
      -

      How MeterX measures

      One person built MeterX to answer a plain question honestly: is the connection I pay for the connection I get?

      +

      About MeterX

      One person built MeterX to answer a plain question honestly: is the connection I pay for the connection I get?

      -

      The test

      -

      A full test takes about twenty seconds. It first measures ping and jitter with a run of small requests. It then downloads with four parallel streams for a fixed time and uploads with three, so one slow stream cannot hide a fast line. The first moments of each phase, while the connection speeds up, and the last moments are left out, so the figure is the steady rate.

      -

      Latency under load

      -

      While the line is full, MeterX keeps pinging. The difference between the ping at rest and the ping under load is bufferbloat, the reason a call stutters when someone else starts a download. It is graded from A, almost no rise, to F, a rise of several hundred milliseconds.

      -

      Where it tests

      -

      By default MeterX tests against a small Cloudflare Worker, so every test goes to the nearest of Cloudflare's locations. The worker sends and receives test bytes and keeps nothing. You can also run the self-hosted server from the repository on your own machine or network and point the extension at it.

      -

      What it keeps

      -

      Up to 200 results, in your browser's own storage. Nothing is sent anywhere else, and there is no account. You can export them as a PDF report, CSV or JSON at any time.

      +

      How it measures

      +

      A twenty second test in four phases, with latency measured under load. The details are on How it works.

      Open source

      The extension, the worker and the self-hosted server are open source under AGPL-3.0 at github.com/ajithakdev/meterx. Bug reports and ideas go in the issues there.

      Who made it

      Ajith Kumar K, a backend engineer. More of his work is at ajithakdev.com.

      - - + diff --git a/apps/site/static/faq/index.html b/apps/site/static/faq/index.html new file mode 100644 index 0000000..1057c09 --- /dev/null +++ b/apps/site/static/faq/index.html @@ -0,0 +1,41 @@ + + + + + +MeterX questions and answers + + + + + + + + + + + + + + + + +
      +
      +

      Questions, answered

      +

      Short answers about MeterX. Anything missing? Ask in the issue tracker.

      +
      +
      +
      Is it free?

      Yes, with no paid tier, no account and no ads. The code is open source under AGPL-3.0.

      +
      Which browsers does it run in?

      Firefox, Microsoft Edge and Opera today, and Chrome soon. It also runs in other browsers built on Chromium. Get it for your browser.

      +
      What leaves my browser?

      Only the test traffic: bytes downloaded from and uploaded to the test server, and the pings. Results are stored in your browser and nowhere else. The privacy page has the details.

      +
      Why is my result different from other speed tests?

      Each test uses its own servers, number of streams and length. MeterX tests against the nearest Cloudflare location with several streams for a fixed time and trims the slow start. How it measures.

      +
      What is bufferbloat?

      When a line is busy, routers can queue too much data, so every request waits in line. Speed looks fine but calls and games lag. MeterX measures ping while the line is full and grades the rise from A to F.

      +
      Can I use my own test server?

      Yes. The repository includes a small server you can run on your own machine or network, and the settings page lets you point MeterX at it.

      +
      How do I share a result?

      Press Share in the popup. The link carries the result inside it, after the # sign, so it never reaches a server, and this site shows it to whoever opens it.

      +
      Where do I report a bug?

      In the issue tracker on GitHub.

      +
      +
      + + + diff --git a/apps/site/static/features/index.html b/apps/site/static/features/index.html new file mode 100644 index 0000000..86ef2d4 --- /dev/null +++ b/apps/site/static/features/index.html @@ -0,0 +1,81 @@ + + + + + +MeterX features: history, plan comparison, reports and privacy + + + + + + + + + + + + + + + + +
      +
      +

      Everything MeterX does

      +

      These are the real extension pages running on this site with sample data. Click around: nothing here touches your own results.

      +
      + +
      +
      +
      The side panel, with sample data.
      +
      +

      History, trend and your plan

      +

      The side panel keeps up to 200 results, charts them, and compares the averages with the speeds you pay for.

      +
        +
      • RangesThe last 24 hours, 7 days, 30 days or everything.
      • +
      • Against your planWhat share of the advertised download and upload actually arrives.
      • +
      • Run a test from hereThe panel switches to live figures while a test runs, then back to your history.
      • +
      • ExportTry it: the PDF, CSV and JSON buttons download real files made from the sample data.
      • +
      +
      +
      +
      + +
      +
      +
      +

      Reports you can hand to your provider

      +

      A PDF with averages, ranges, a trend chart and every result. Each page carries a report ID that changes if any figure is edited.

      +
        +
      • CSVOne row per test, units in every column name, local time next to UTC. Opens straight in Excel.
      • +
      • JSONThe same fields plus a summary, with a versioned format for scripts.
      • +
      +
      +
      First page of a MeterX connection report with averages, latency under load, a plan comparison and a speed chart
      Page one of a report, made from sample data.
      +
      +
      + +
      +
      +

      Settings, schedule and your own server

      +

      Choose the test server, enter your plan, and run tests on a timer with a notice when quality drops to Poor.

      +
      + +

      The settings page, with sample settings. Saving here changes nothing on your device.

      +
      + +
      +

      Private by design

      +
        +
      • No accountInstall it and press the icon. There is nothing to sign up for.
      • +
      • Results stay in your browserUp to 200 results in the extension's own storage, and nowhere else.
      • +
      • No logs on the serverThe test server sends and receives test bytes and keeps nothing.
      • +
      • Open sourceEvery line is public under AGPL-3.0. Read the code.
      • +
      +

      Get MeterX, free

      +
      +
      + + + diff --git a/apps/site/static/get/index.html b/apps/site/static/get/index.html index d41a141..b3d2b7a 100644 --- a/apps/site/static/get/index.html +++ b/apps/site/static/get/index.html @@ -18,17 +18,10 @@ - - +
      -

      Get MeterX

      Taking you to MeterX for your browser.

      +

      Get MeterX

      Taking you to MeterX for your browser.

      - - + diff --git a/apps/site/static/how-it-works/index.html b/apps/site/static/how-it-works/index.html new file mode 100644 index 0000000..b7217bc --- /dev/null +++ b/apps/site/static/how-it-works/index.html @@ -0,0 +1,74 @@ + + + + + +How MeterX measures your connection + + + + + + + + + + + + + + + + +
      +
      +

      How MeterX measures

      +

      A full test takes about twenty seconds and runs in four phases. Each one answers a different question about your line.

      +
      + +
      +

      Where the twenty seconds go

      Segments are sized by how long each phase takes.

      + +
        +
      • Connect

        MeterX opens a connection to the nearest Cloudflare location, or to your own server. Under a second.

      • +
      • Ping and jitter

        A run of small requests measures the delay and how much it wobbles from one request to the next. Requests that never return count as loss.

      • +
      • Download

        Four parallel streams fill your line for a fixed time. The slow start and the last moments are trimmed, so the figure is the steady speed.

      • +
      • Upload

        Three streams do the same upward while pings keep running, to catch the delay that builds under load.

      • +
      +
      + +
      +

      Latency under load

      A line can look fast and still stutter on a call while something downloads. That is bufferbloat: routers queue too much data, so every request waits in line.

      +

      MeterX keeps pinging during the download and upload and grades the worst rise above the ping at rest.

      + + + + + + + + + + +
      How the load grade is set
      GradeRise in ping under loadWhat you notice
      A5 ms or lessNothing, even on calls during big downloads
      B30 ms or lessAlmost nothing
      C60 ms or lessSmall hiccups in games and calls when the line is busy
      D200 ms or lessCalls stutter while someone downloads
      FMore than 200 msEverything lags when the line is busy
      +
      + +
      +

      Where it tests

      +
        +
      • The nearest Cloudflare locationBy default, a small Cloudflare Worker answers from the location closest to you. It sends and receives test bytes and keeps nothing.
      • +
      • Or your own serverRun the self-hosted server from the repository on your machine or network, and point MeterX at it in settings.
      • +
      • Why results differ from other testsEach test uses its own servers, number of streams and length. MeterX uses several streams for a fixed time, which is close to how your browser loads pages and files.
      • +
      • What a quality label meansExcellent needs 50 Mbps down, 20 up and under 30 ms ping. Good needs 20, 10 and under 60 ms. Fair needs 5, 2 and under 150 ms.
      • +
      +

      Get MeterX, free

      +
      +
      + + + diff --git a/apps/site/static/img/shot-history-dark.png b/apps/site/static/img/shot-history-dark.png deleted file mode 100644 index da44d178f6a2fae68d17bf0da9262f3abdaedf01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59522 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%0Ds!( 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
      TC>ZA8k6YU2MGA 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 diff --git a/apps/site/static/img/shot-history-light.png b/apps/site/static/img/shot-history-light.png deleted file mode 100644 index 2a709f65ee26b68cd507760499ad59f4daf500d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56986 zcmc$`WmFtdw=LQU0TKu#1Ph)(a0~7p+&yTA;L;FW8VedcxH}DvyGsWr5WH~*4NiCFfiri-U8=*iItTiRn;`&5);ky6mP8YN;C`& zbz~da4O#x3RVYMGQ{9IWhvmSkV!qnj%d*kqe34W6iUORoQdT2RIK0b!jr{k~1kaBcIQWK( zs{oe-{ZxH!_3vwT3^6R=@PGElus)4=KR>^_JHMQjm6({XDIfn0H4#9%u(rOowuX)As9|~INKxg~C!n7hJVd2%URG8S+C&;u zcRzNG?yu1O#I(`9UdVb@?rV7s3CT-w+IiPz($wbycmg$uT+x z7A5)NKY#jv5B+AQqeG6Ul;@4xsRmBOeQD=S|yM7rZE z1lrF0b? zR4`TcfThI5#MD1HXq+dPB37E$06cS|S@@Z%{J()_{k}$Jf5#qN+gO*qG^reWX~pO6 z&dFO`m>qOK23#RaD?5}}GV9IqwlUFmw6037c0yvJGF5F8zwUn)GIMTmsVuCv#WVS( z)gJvgazbOBZRZ8>!`i5p7D3eaQf@N}QgV3&$P>(GVv3TmP!=y#;OskVjktJA-FGF1 zvVlk?H$8oguIK0$|K6;xuB@!=>i~?fP$K$hLls7x78+DkecFF}){6dOZQCrG zBBfh6Jp;q+XFccLG5g+MW5YO1QLBn?gz22-8(=f6W@GosntXNNhf-S06_P`nOw zR7WdM2_B?4stc5D#~R+(x4_|7!M{09PB96FGxxW?@VK^*B_%bqHrG?r&}1T$kb= zp6~j`AvMC9oXPee!B_{wo^dBWE8jbNS-`EbLURXGF=o5E zY14#!u4{e1s5!8=%1Z^Iq1>EXP2?nBS+_wS-^XijD`sWR{rYV`;=9|{F3Fyqv@q6^%ZnjY$tsyv!+pp?51~b_cFzU zTpc&|0-|@mGa6XOC(?lS;x=?;Wd&**WxM1Y7AD`wgOO2)ORY#~K5%Eru#5L-6#-ZM)U&fr;TN z{?t$8YPKO^lg04*aN6>K#d*d_ugZoi;;F!t>skWT&1;bEjjM?M2S1930h)B@A5qs0)IvP?y8I z!KCOrS^_*ga$cvsp;q*}K;&kb{;jmCD$W!QBO`S8V&S+Gw-9rqK2PaldDVR0AoS~* zYj!!^@sjP7$nCWVwIF4v?d1abJ-chuhhyTut13*>_lHjos$b&}IbUtb?$mA!r@tr9 zyZW%lC?_Y!ZL|0=HXaP6JP&rleEw3mZjEr(VmU$kvW=3J)u)qU&Ukg@8k2gD(c+$Q z(kA>zxY;4ihYxC;o+p1=rR$X_5xOoe7yY%V5}h55)DPdqoN^O|>@GOH39(e-i|kc$ z2U!{HRcY}F35U_Rc80hi3$mFmLIw*IJw6a~#^5h!$sXoX4MI)!14=tY>7@JU zXZ!1azsMul@&72Lx4NC*KxQZHXl;73A@lUHJpz2w)nJAiZjVF@IiHyaFAskHSy+L& zpx1`|%Gvd5!BDO3%%8r#pI$5R*7bC-(Wzg3tZ3sD8j1#kbDIG7YxTK{{@y<;qu;k?Dk)*h zg@&Hhjo%Wze%tamqUybjmBpZWcnaU%dET~Acr&=z3VnPs@faT;@2$r^JOTUO)YR10 zM(#|fPJ1sJrKTJ^LenvCW&a0#K4@&@fkXLM_&9uW znrQCT@@aZ%>=pE;kyPMD?=Xl|r%=HV2LDWmNO&%e!cO#dxv0VE%C)0Kzz>8s10Sh~ zbAp-ooi}Og%WD4ovl!wg&Lkcv!{E4GZ~5;hGKA+JtMe6eu*7fhR4&KzHBX7upXGkn zxPg6D(;1FwDbI3G!6qLG7QTv0e)d?GzB`;DoXq{jV)cUnZ7@U&vaVdkPXMpQ*RoASp51)a{|LPP0Svq-h_=dH*JFEIF}35+o7D(B zzr0^}IKD~Fxj$bhl=_vAb6RW(_eF@ThSFcwf(Yn?!Hz6X>23OriVHekWO3Nv`%N@f z3zWngi<@peZo3Y<{H%AZ>}kssuRk$6JJm>D{LB?jxo647d5xCkpi%)pT>AwUCwZ}l z{l*4Ts1b9Cew}<)eSTN;QpT(YU|LP{I;rZDKj30~HeVKk*)aLJ+C`sg9 zZSq@^kdjP(X_}yH9E|*zF$nJd*EV}p^o51x_x?}){R;I7-!M!&`pp%|D=E=C2Wrga zE~fRIzYUSS!YzFK>Ux;F{DOO3rODan{d9e~W3S7dw)5%+8BcvL?4cx}>(lJn<^cl& zfr;7bc7t1S%E#BpIU*qvDaGI~E}mR}_?~M1JUGASs}HKU-o#yVNQi9sH%HHz&~r*= zjw<#*tcv)T@qs;uU4;C6)2P7#Jv|A08+7hqF_aq{8sh-e_dc^{FR@{RZ!b{9AFs&v zo9|mV<4n^#Y(d0+g5umCQ4;Uw|t!*UKirYGgI>g+4lH$z~ zYhBKz9iASVS`GZddv_6Z<*W@fE3)L=b%c?pU1Nw8XYn;-qMqU4V={JmYHs2;lW-t2PU zVHhzw7jdEq&+4C@md1p*{2;UlM;Hti#-oJKViGJz)GkaQli~FIzqtVH8a85L0;L6@ z0JXKz7lbqw7Gp+sI@tdGG~qz}5*DrE(&sB=-bEX8S36pK<8>~@FvK@KS7$ZzC&(~4 zkZ%fw!`3bTShVCQAoJ(xGMn_=@;fr z-dDaSmYAmm_nrABDm#|M({eF&pP3kIyM|nz?(A485Pdjgw4HCeJY1BBQ%d1>sjVzC zH#L=2_qgd96u6>IDjD=b6__Qtd|1vYu`sHPc=WK~&jPc$?2?JPXsl+r9IW6o6p#=T zf4k*V{hHe+Y5JJ2s4q!eZTw`u@h#lidLF)<+dFLWjYF^DeYZ0~gnl|beDorxDTP=Q z$;`aFv#Wje+{HcyQ77@3p69L7g81vwZLE6vNv~#kIT|T_nmC^S;qhr<3=q25JKFvP zo)i}hSDTZU>Twm6nlMn;ZIY#RKcr+Y<4{ap8@4(4dl(W&Wr(=)BW#jS9vJP*4rK~c z>i$|n(|tZCf5E&xarTyXtmY3tA~TcEdnwwjY81|(j3jx67CcFK!1SJyjxHeIc_--y z6YKep?=s7qtxD>6t&`Z@l}8ftfjfg`^eqdZ47|>-L?B zm6no(wj8b0bK)8bHs#no7sZE{fekd|o85QG2g7fzK3vk9_R^Ivv~-dU{mvP#jyhya z$yOA#HBvV+#xU6n;UjKtsu-9-5wz5ZV=mybhxEz#1A9y8i{TotIx&{}(H6Z=h&+ne zlmbZ=W%|juIr*MU@Y;vFkP;WjGlTurf`efpK})9GY4127S3&O46@7B+>lh|Bw7Pejuf;_P&fK@~A!K(-U>gMojn|eTN zyf@W@lOfz53l9Fs&MBj$G*fZygJiA1EHu|gZ2NRb>z%yvuo~-&mt#U6U$>qV7(Isy zkhnC~)e%4{)Xa}nrq6$n5`B12z5IRY+D@5OCwnSCH&=2zwf?Yjm8NE5m*Q>B#{LW| zCI-fg8BjeGl*}8tAlsqK0iyi;-Tu^wC)0e)7VljxyLHr1cF%>YG!% zbj&wtW2vk48pkPeZccT(f!e*rTJ`3-Qj2tSY_inr(-jJuLH%kYM*NS{xuRESSX+wY zz|I;GNri8HzS*~EFdu&CRL*D`S2)d&c~E5K8}k73=PQv$cUx`ZdRLXOsLcpH*O@5p z*1TTjOuOlpbT2vK8;oqbUT;g)DM2>2?I?)P1Fz{&jm~MIj;*0>qSF2qW8M?}ix{+B zZrf((-Uy*O-^oUgdovG~`{ahh=e`VDSV6+u##C)T*(ZN_w+3UmAHJajXu9fr<^19J zf<#tG>@kHyMg9Fn>gAA*vN2K3(ZnuD>f4bE?nA>{2uxKaH;V2Yby6!qp?jjbL|z?? zm_nA0_urmxzrVJ&UMh12fVy)oP@#_ZkwKz2Hwj0^gRu{MPR_0ih8G@ZiaQJ`%}%-w zxgj&jF)2+homvOmKn)MktIumo)e}cxHZywZ2uR3Z;m-8bq_vq@pv*3G#(Egt>|Yh< zNSiC9H&`v0c|1*8%L>zZpL{~8 zPLl=8ZEwn1CvOq;h^y#lw+~!-cIDCFm)Yh9Z8wI|;gn>hKRp&53Cl(z^pxht$Gva9 z3oz1%^1)dITsPt(^jw~y1y+0yDVMnA&TV^3`M`AJ)POk3TWbefXbykva6MGAJ0A5y zU4JVlK0FV;Yd$;zQfx%TORC&w5_fpK7M{MsL%m8F^eikplQ3iELq>S`Wm1EFY!gzs2n9;X7_$0ixrCdc*lV}>jvv6horB~rfIv8GD%&PP8wx<-ToCzJV z+BSe4rbv~s+LQzG>r2?{&;_XduCD;Pduru@=d?O z1a;!n#l~P*Ih2!|-)_08;VNL1DtNSl3+0{WvZ`Mz-?`h=+&vyIcA1vj_V(a`w<^cT zq+@2rf`!>_=3_;(_IMpDKC{Iyzt%3rOP2JvAb10CWuO2qJzV>lmAxjoN z<8}_Lxou!^Qt@>c`?_qPDy+oZvTGnpS)}DC7H=qEv74wMT3YT-6U)kU>vh)N6U)45 zTq6X@XIgYQ{GFW<+{!3V zZhTUy$sU)yiS zaXyfJikjk(a&BwJ+mdcMoF5c!BR{|*xJ}@?W3(6cH(ZSbIjX(AQY2C(kxo-L`;V_j z5<6@s^M!Nf<>ike$L0eHO{!fH{4OF|Cl}oLRz3tuGroWSo`pq8TYIX#y?u7JNwZiB>~$Uw zv{}AC`I&zN=S|t!-uXDDlJ2J~uKQ4*+9D{yA-vLvd;6J*)KYIpB9jqLoloeAm>CeF zOD}xRh+E4Mma5gPYT(PDJ|nh&vOa)Gz~pv+<6Ia;48)r>Ui+J~ZQ!r)XD6qpcJmF( z+uPflo13evxTK^uU%q_#`gLV%Yq}ha$Pj|cHDpx9#l@+7(yO;6Gp$u4X=rLH)2%IX z+#Grdl96S`vMSUG^iL3a6>@u}EzsncKCw#Lo#>O0m5{EDH2+;4b?clmV@UxX`6Ol- z`2@rkPl&|8z#w4Ot%1zdy{g+o7LJeRN^@~>4Opt{>w9jEWOG?hs_-%vV|BOTGis@f zhF#PB{{4HkHwxi#zJsnuF6`;{_t$q|Y=0+z-E+9xCSgMOW9P2A8Zn=fz()B1PPk1{ z-S}ayPx|KfZF%fxA3-X$bZ0U#V}0oYu0bz}Vv>_}v``rOYOSXtt?btGIAfBMWFRz( zDP0=+hKA&(tg>pv$sdixb-2VqS1m=o?axw&1B9_;7|*j+$#zu&f9FRwMHDs_#dv=D zYAfnA2^n55MtdI1XqW46uf_IE7pj5G&3`^IqQ@B%7mMioFLxiKgFk0-q4qMXjo-zIPO z8KKSmJ1}9!cl^)}t;4J&o9J~8biL6CL;(9ZI8nH8EFr<*il03U@kv|ac%w)Quda&( z`V96;_kg9%bChoGMJsdC*oYmQ4NTfW>WPohFilW%LGwq@QCt7po4nbihzLck$<(OE zwzh{t^@0>0JC-pfVPWs-3ZsJqJK?{08=jt?7FTL{GbK9J4|k`^2W4q#q|DhEY01e% z-Sj>_J|2_DtG%q?wm-G1nwqfC#H*ryhs%QnVXvESu9mgtLurGIBMh#I zRk@|5%qXDD{K@^?x^I~x_&+}pVB46sK-BzCGT)}gOhJEhM0-jsZ_jMFsLdma8v?Gs z!q5?ar5QyU;g;j<{RYN79*ZXm1UhbCjbaHd*6$^~Iylf-T?u82d9-Vp7z>^vb>nf6 z5BHZ~Z!znICg)gMWtLj%_(|P*TLTS^u0?~2FYO(HnAccC8A7+~iC_m;dhv8&uO`!h z1j=1{dirdf;rrW5M|tnZ`^#hwbM~0*l^K|P;g9h z+SW;}3$wGuIuqxM#Kpzm{yuW$;^5{c=dso2iwMSK1HCIY5rQuqzbO?N{q=Q;Kq}m% z28kE$6o)%@a*dfAd-~S$dB{L=Q)eO~uPv}=BjkL*Dii$K;X$^guLImxVe~;llYc3l z3_t?9KunFBXlMp@@p}CxPZrzA?d73kbC^36>L}0@{cwMqh$!NB-uZSK5)u+_LN4Ud zHK<;zU_hql1e!=HLFameGeuMH6K`!>Y5Xi61eymE0*%p`om9 zxwI$6AZHp$vveVs6RrOcW9Bl8#WWx={s!dfGBb0y&y-zW4)%uP#$LKic-`M{5hCGu z^%kR)2k(pE@ra^;fPgA3y@ZaAj^I}m4zAGioo>-y$o{1ZpQ4fy3!=4;3p6&ixZkM@ z{t>MJL{uN`>L4rq9Zs=?i#5LXzRTs*%r^>i#L_Kxzt^Kv_%l~tPFOn zIO2|PufU*3*= z$6S=ld}-W3z0Zn;vFr;D>qH8lko=;K{z~k15k~6jca&_2h^{f2iFwh$^eE*45f9dj z6zQDa+r$UXG1WRYO%mj6fUT}o1A)M=O(Qe29za{+P-@uPl-5I85OMz_OG-JJ-&?N*Far6co0Zsdp~h03Z~t(8n}2~uyL80cF@&-*8i)Z-Lulh??8u#tjjyw1Q)roW zF00MCZcc8ialgr)p@$70l`By@Vg3!OKQ3*&b z!uZ&qP5NL9F*G#1i_6ae1THIMz*7N<|3Q6abyYh2?R&N7Oansoz3YM=mk$zXpuD0U zG+-C(MHdTol~YdCMn5@y5yOE&r+S=NV?RbVy4<-r^;*O|HMdd9Q0!QH7tA6+nv|}s z@i1OUt%(k0@z~i&?Er~RylI@>m~V8vmez7GZ1p&w&;}nz?V1Eo6@)(n`jr<43%ffz z{I>HABO@xdjsNn^;TQ~)iYQuw;xm949u5(Y77eFB+5z%RE>Woe-Dy2FEo~GLM}9-Y zmH)2K!7)26Pdzf2&Q#d@B<%^Puqat<|0q4FeIPSzDieNiEiNK>D&X&yn(v@kjQ?%!E52`~%igQ#&cFCIsK2(@@hE7r|3fr|cbSC3cCY^1`XtAjG&D5#O`eJhe^ULG zJ3FbiFSdSH2O|EAO^vVD15$~Z`@`>l{_s%s;Y6zjM`d_kU2Wera&U4k#?h|gz9D4M zX~*i=*d)wD^dCXjmCGRy^V=^)vD9$=bb-xAVJ`lteGtex<-N&UWK6b!3nX^32BL28 zSZn3?fOXCo^$SCbNg6AGf(B&0o1G^1#*P!obpMs5=%0(VNJo zr@k60C4A6}y%a8M0Q3#3mE(>8!2^IANl8gZQD-%EEMAT*l$2kQa8*{rmcOh{_V(Ef z+CWT6j-qxDUK~mnNL*?IY9@V^u!snlpFUr^lDytPGKQIVdEo(0N!p#hlHYg6%`F$}+N(_ZgQjLw;5kfFu0?fC%6OCV*Q#wB5h5>V7otc^UnrEp!E)3kwU&z+mBI?o`^0 z-cq%T>5+cF@XN+!%GYEW6-Ou?FL8}QYQnmp>&4mcA*@AxaIt%J&n&<0fL2KX`{ys; zexwgJ^Xh462wKnP-6+`&xg+y69Tl<8xk&1S9er>J$W@2cDl+ZA$^o1cUk<1ksl^_;gU2PEQ=K1R!!CTAVD>3I&70->gVdiq6w+smfaI5 zA@~rfvNR!AOleIA%Q4G>)!fr=z7mRO8`su3j_5?D9~x+LG8Z4s$6h^QjvxY{mX(#2 zac?B^?x=w4!M9V$Y|V06-_Y;DK{9re=OQqow{KN+cGWQ~Ngv{+s;jDieAK@*z|6!H zo?4K=&(9CzX2eP8qx)TJHEJ{5)(J4e<@YE{0Uz48Oq|?4#rgROb~pU^0au$E8G$`X z9Q%yzQipv}$NBjhAo0Cbawi<>1*LvvX!3yMGpuMqEGBEXg*z@x8&ZOh<)@aTuli2#? zLZ`s|w^yw)pR5n|bSqCQx2@=cS`nW#b-Yb_G4_}Q8zO$ETq-YL2xk4^$T$>mJKH(e z;~cNoz8uXBil1N-L3|lQ^0{C8ohI2V)W|9Gt{op8S3mCGHCGl7q$P{6fmCn+;I%4e zisuo8g?oR7ii&z1g%87+1nS1UeOMide@lyJfl|6f`rg9h!)+AVPiD=pUnv3NX1GKv z5_^BPLCEuDE&0~fSX$jQtZMrn-EUQBuAb5ES0(agFS*pu`v-c-tKRPpX93LBRg0v; zA|_uDcXxNzJO^pKUKs}Bhc2;FSm;<=#@5E&AmqD=lAHwaXLh6F%O+gYd`_3BGWSkM zUecIaHOAp>i)&ld5Y{<>8(qrY8C1#>a@J00X%~vtmdEFAMN)0^1$I+YxJ~*?E6d6C z+k7%$P8|bfky=lcO}GsV4V9G?i>p3Q<+3Jlt=BF$TnYBHU2NfFJzNm3)vL3)Zipl3 zrE)MqdBh_DmiO7&8Iv_Nkk;2kT#x>?hd)ar;oaG>*j!r^_TEu)1$$hZn3-8xS#d)4 zbcd=Na(S=E#!$`CMm--o3gL>(H+4l$x%{;&xXD&4n3-aH~mR8kCeZ zCfI|A<12#wFd_{>wd!?;O|KI{hOY1UUm=wSbB99{7xzeJ`S44 zMu=BEi(hqfdh55qCvdT%@sZ6j|0l2v4F#o0qKV3(j%bbSqB!v8pWMqHq4o8o3`MMD zj4{IIUvMQJb8+SG8+k=sRw1tveO}Dmn%RIX5p? z;>f%E%O!wb>|2b8iYmzBvs-Lg6bH&pKywd}1d)-E8zR=%C+jhp`t|Y!7yrZ`Cl_I1 z%5EDEF$qpPN8i2n0Kh;=najM>HrLGak**6+B@i}Al^v3)uW#>@kPzBgogBy%HZ%kf zvZ6Alauevk66XD|2Q|NQ6D6R%rcya{;@C4j5(CfMlZfh1IQ{ z)M2AxfBUDR{u2Qohlegkw*)F-=EsmnXu2`39)!6TgCZ-d`28$OPrioDAGVMAaq?Hm zTSalRNel0jge0%VH(3E0ZCt!0_?J+9@LB817jt>NCP_(H=Ei={KD6F;2~ZtPOpQB# ztJs|@7C>a0UYhOBv<@6)qYN1|hsz zNVE7Kb;c~Kq$K=7!|E1bEqyK*T*JE*!Xs=TC4PPqz+Fj73NVC>_BP%XsI)>RnnZfM zxvDh7%w}e07Td}nZ=kHq<%eruG!I)h2?z6R_&SKam#j$6U4dRkIe{N-T+!%Iw8^+2rs=3H9FfntY zJ{+wT*;}qInZP@7pG2X(q;{4x*nkwwr+k_td3p44LY@BcIr9#`WoKMdBOuofwL4sF zWaL#THfYv@Vau+qZ`#dQbuO$9USUIc#`i~8&4C`+n*LJs5<3r1qQ}I~5guAnB^jBP z*q>MP_v?3=1&%^A)Ya?7+(*)!eiGu_QoG>uTS`SN*OBZuhPS^y&{6`}Z~oh)IZ)QjSVx1_%aIfL5UXV=pvuv#Q9PZMoTQ+8xMmMCv~C zN4<|3jfgA~1W{lePA3{0h)d>tS{kxG`tG=OgE>Nj^=`67O`X`CDD8b!Uc2;tml8`( zFNIs6naMhVY+I181yGkIxAmiEPBa<|Y}})4>tq`($1Ph_WI=WbV8Ww^@ggwzqEIu7 z4zKxTq(}nGO|q3GkeoEcb4!i9*VL>OC8q{Uty%>4}Fo)=~>-T2Zjrdjwgy?O&j3Iw{ixrUGM4C zmPWlNsw6eijDv*%n+DuJ2fxhVy0}s`&{grtPt%F4oDHHF`)wV+K5^pXJM$oGeEq$c zmAJk)n^8@D-$P?Ot!afGgkAWqyQL4iz2`dhV+zbS@zcFNcudw1VDnRWe(k-0D5de! zw%8s+r#6gp^~Pz|(=UH`w6X@Z%?x%AO?n*r@o{Xj$SDv1Yc4}b9S4D$HTdO@w13i` z!H#I5eERh%C5F+I-Td}c0Xo$*5p`U?5Y%ZI0~(3#5=-kX;Jk5%9+lzEVU}046D&dN z%VfbC7ahI4yGue!N{WvUD=)XRu-Mw$)8CPNer5K8Ei*jSMMp;`rp9Kz;TWJ+Eanpo zAu&HwQc|L$qk%p_DmUcAusNfT9$i(y*YU-LM_jw@FLuzsOJsM{tGyA~BmxMV->l)! z;NDWHKi2En*xA$guc2sJ`Ne==;k@$8aiBHA+392A$!I$41P{{+t5nz_vxueah7w(( zL*!G9bI$)4&}>VfxAJk8G&Cfw z;0!uh%r%v8!3ztqiKY_~p9~aBp@~Z7L*N|M7XTP-^FHzwzDr0+Azt8j74+HO!Bn>6 zhSSqwC{vN&8crg|C-ApNo)lpgmDRraT7)9-9$;|Z>rmV%3^24DGI~5$#?{+zhbScV z6TXdQX6Lsw&B@PGspNp$qt&qIi@)=EQF>RfFWh+Dy)JSr`K{-GF`5z+vJ01YdoqsQ zfwDcL9*oe*F5rPKQbqfhUi}%|`2ltuSv35n247pde4spM+6}c!6yU>X!(UlhiW8ra zKUC$P2TCrWX4P26H3*t~pgKJAe<9g!i{8J=RmA!kUQEJt1rf_wx zxucwPP5VOi6#k+LPqUXy{7Fe zat`2jRM3F=aXMwDhLbgtI9}Kbo>sQ)nQ)h+e^O;-GOXFjg+!LN-Bw1p|zi{S7ifuh~Pj_2)#dpk*O+JC~rLjipS0sWAq6oxk;?lL{&>B-5{rxI&x zZ#U7D%2==(ssX*wk=tgO&?c(^iC{%$->ZMtLaCl$Vava)oCl5vzcq-&N?kcR8#_yq zl`A&%3PLj|^RgYiE@nN47r<58UY1iEC<0=HczBCVj!}q!4DW{(&E?s}<{>e_Cy)+3 zOn=DD%UioCZRQ-Go(RmB?Cvg}vNNo;t?r9AO5wK0f5`GHu?dL;1I8aX#DpIXGsXXy z8SkbtKKfG}%VEdYW4FyQuK0@Wg%p23(5sT+wH=_PrJYi05O_>JO+@jBH&9PidZnIZ z<^VPL<47eSBvQYQsL~Mqax}Iym2YlhVQuNWja14zevo)n4FZaPs+Wd(5A{G#S5;fP z09vmUe)LWCg0yk&@h*74iu23OQssE~U%?#RS=|QSf37up6OS!*L0Zjo%)V%E$q240 zV=@V{bxmpiyWA>j&;Fz#Gfb&j@2-jB&t$W;ZQr(uQ)BgsdA*K06v^i5FV0ScJXT&V zGz2pA_p8(Tl$Sj$hNzP6+2Fp3jU*M29CT6ilzwhyW8imLE@!+R zhCNZ=iviw;3IWk8D|}vipY;~sehjNTJt~wYp=0GasL;;IX@FnOn`7=12Zhud4}U$9 zOKZ!L!kcKE2+gVak_;FsKxo%n-?v`?A#)&cXsP<}2!L+zr@*xQU9(tfHs^V$71jX+ zmy8L_)2@FJ<_D}{p`n0(tgHOIu)LtzVFu7S4=p%C<8flo#CpK0uW)cE;-&vN>|jT}cwr726l{cp;G+Ma`~&@}$b z1REE}X!ZY0W#{DB2;q}on=$d}oieW&{^#7V|J$wo|MKAi&(%qA*F-5O7=!o(v>^2K z`WZlUqPT8R(F8m+IaIM=kB1%+WD2-k)v~pn1=tznCU5R0(U>EnYg zuN@8Z)Wqnu>5vm2AKDc$k3)97q`G?g6W=KI$4#QUWw07XI0o{SlYeQXBpNPG z*)3{ns*0(g_*!YHs%vP})K%flm+Y46Ai59ZeLSRs>JkXBgc^7S*p7sS3=IBjbqi@$ zXZ2Wa>4Oy&xoBy5K5(Y(K=7t$ly3pAu=7-;q78~mNNEHrydh)$STtFZ-8|9Xmh$ysV?Rb0> zAZ~+$rAp>qy1Xe2buUv4*`CYdcvaIgY)r z%K=i6(45MWnx=a3kCC)g|NPkrFm$AT#?p4Eg*LC58_?Ndrt-VY!4{L3Q@ z|Mb`T;Njy}x&lhz?Frk3HUV!p%5JXc9{c0t$33B>m=vw47A09(Hlak@)wYK!=A^-JbCS9=k^I+X2;6_~b_Rm#;S zA;>CdhZx!7dB@ICZPAlc=FT?=M>IRF-_&H%)YvQr9H?>6Hn^iJ4yQG9T6Mjy!#Xd0 zJP=Ue09ruX^OdcY(e1mw@9NQ`3b9-@ECc5qp`W-K6?eT0M77k^s{Wb#s?Gt60P+;5 zDLz1q#z8FjPqj;LsQ~{?zZ=R;+qO~pQj0u&#EsKzi|uTerm}J(gQq#Gezv#gzinJ= zgCi83a@~skMV%P(BZ`PCt;qI_alZBOr|ml8W50Q7-9cVqu0PpC4=LIH*f>Xq@M#tk z^bXkwFjpaQmloNr>4TxBn1vvn&!644`>`-m(4$E0#)}$*&@P}TZozeDTgDbPCcFTV zK}(BHEh)K^3lvy{|1r{yQncNsKwPd6v2kN1rA(FrKZOz3?$^sW(a6WEtG1@czxIF( zbgNUcUg-9fy4^^v`tA37DB@E6;zb>;UcJ>~sU!N1i3L^v)Zux?y>w(IKjXtSs&7hm zO1gB!%2P?wqPy`Qdxz#I4QdE5xNh9ZY^hPvm+m$br&8O2zUv3 z1V|ci6xlTb4mHl@xr_w?$M^ya06J!{Z$srQIaz6GrFBB{0n4%?N%I*0OogNnSL$#} z>;1I(sCCxje_U?*k8MJOJ4s_Qwi1bGr)J79cjE7`n%=rRaArQ|>&CyEL^b3XF345o zw~vZIRP6ULI$#x~Ur?iumJ+vorf#4(i0n$@?wVOT>p$0{+{eeqD^R-mPDYcTsK`uD zp#CDZ?fYAhTUh^QREA)|tGy0)j=5ThX;!&3^_>euSM9yNtZZ~$+nd;!nS}QD*_`(~ z1FR`L5QR)8FRxZ09!B1YkI*kPIM?50#{B##lL?;nNE2M1yHUbMs*i5gTW?|mU=6UP zx4CZ$qky08YJmNpU?+>)#YK`mRSmYvW4v%W5mod{z($*Jw#(e) zuf4P=RAz`6)F6ck?G!dP|8rlft*+n#X#vNFTK|=jF$EHHcgupuZZc^p{EZsmNrX3? zuO&l+T}tUyuWwp&)+Nx-0S`Q@`75&d$8E zfZnyN9TipY^t8$yVK%*_5E^LltBAl8vG*$!i&p{9q#8Jgp!JZ{go0tiJLqux+8Q2I zK+oA(vq&wB6rw_~b4j<@dhzPcc_u}bDrjqW8}KTQ{`BiZBLN{lPV5O<;ln=9Dw*AJ z{x9a`cwLPSbBhwg3^q#Fil5RmR} zc+a`+=U&g=&szJ#^SD>Ee)v?z?ZYwMz zwBw4cM9MJ|k%})Ei!D`DRNC(3q(0#=x8G>pfg92AV#LW0;ON_2UVMHgm_yET>j}KF4Tq7-Gz!vI9cx zc5msJ?GxClPypLsT612$vxrA`jDH^^cl^T~C+&X5hC-ZT0`2k`U2p>}mGAPZ*aasF z1=%A5RaJM5pUcMC;Y^3+(R$0F#G9L2=~)ee>LIs3$+oJ^@Nv^}+qRfx>S#|(OZz+6 ztQjkJ737rPmoeb5ghT>L+H}h<(a33i+mDRifG>fj0$q;1{gIeh(?xGjFrnDGPD+%* z;m*sw|D)xhq0>8P%Om3)cb>UDqs#EQd=Yn|!ZI##BxA-8@^EPAeLy+NC#@*Z4^(e& zmYXjOLlb98*SGm~7ej;HK9n(U7<&%$nsw zh>n(?*qS^0{RJTy4iL+QMMNNEM46R!BS*Y+r$IZXeq zE+oncp}-h;&@(dDV=&((|BMP;hi*D)>2mxpo1@MX$V%97zrHU6`_FqA{L6hopAs4_5A}s*E7?pB5)E3x*S$1d6#6Y1v2IgpqF zav=VkyCn@7zsoFy5J`zT>3jfFsn$Q6q?H>pOtJ41GcK&DcI*!M#P=%agp!J*3J3*w z#vmX_4%pn3J5Hf|aNm9tMnQB6D_#RU6&e72jJSo`UnEYNlgFw?5aTnI_ zcka}_dH2p-*3|Svi#YlFb)m6J8@y|Q@n23yy%?;f!;}b3hUEm!^$WAs3p+$VWgvC-flv4ag_Z`llCyS7O~$4UJ1Vy+Q@Y;1d({+D!!j6*>&j~UC;zTKqMGqd ziQYnD`BR_k@rjLrq%mK=i{vk?@n7C=OP}J@pOrHBZh+ecmtaX&o`Q-WG3-z z4?bKq6`sT04yv+T6B2n}l<4mM@#DCh;1@V+(XtD*I;nwR)8#(xBO_ZLo~BpC;UsV$ zCWSuq<4Pxm2znJea=f5mH{_z-{Qd_`1q$c0pE|xuMI9#pY37)eWrfPrjaf{7h8J3p8fDsBrmPIvKBa&5>66d`QGQ|ZsrLMHDB^JZk&4>c=2Y_X zmczk306ntQz4nxhUGPqC9{@uKyeFu?i@mwFH+Pe{o(#7_#M2X`lL|DBWD+iC3-IGa zav5TBZEaOA474{^jE6`}Xx2DTK7LHkEI2`R((ys|!r^W~eunL4yVt9wa*8+Con&E36;|KbZB+`?Q`ldW3c)(a<|U&4ZDp4YxT z=I?#$a8P98Wq~D1A#X1y;o@-O?J8Vw>$-OI4}%JW_x~eZ>VJ_>E56Slbag!gb?VXK zVGU^PXWGIW8X7=8q^G9`8m_g0T+RAV_|)ohE-t45q}+V`{3pl9Ak=pE@W8)*-9Q-{ zjzzKH6hIjm0Iv#t6(1ifRD1dIB?#7Kl$4ZY07?@OK+1^{A$vL383S1qmAbO5Y$w#) z;Zd{9@1Z3Bki+6{b7KH0Bt=UtX#H81-ml*x#gwEh@1|IIdU<8P&Yx-yq6AlkZt()V zq%cOcMNGO=O`CYJp{QC9l$B!Rdb6w0D6-X&5 zKcnY5kw}-jnuPd|y9>)Ii_pA|yT*&>()k-IK?}S(ULD01ejsM=YXcG zGx#-pe0)Yq&6Kt$&^LcoB?;irAO+3Y0U~6m3aeIDad5GQgs3N>90sBKlm#pkvc`WV$p2;RtnWCv6*yK56& zj0eyf2O9ur5IGg@3&$ac3Q9{;pEr)LfKD=qQe2Y%uTL+0swa(O=2;ea2 z{u8e0GPw!>oAtPt^Pi6^L7_n)XXu~TUZPcxD=aK*de!6)ER0Hx8m9;;!}lO*(Iq1m zuw7MWU|?8X%~Z)1A;C8D)j?d}&lwaVhUxD|T-m%y8Bgxdr4EshGgPcUFUysDm=<#h zlKz#aRljd5{NhE~0dUYDcn4~b--XLJo*40C&UP32QZYL?&sUPOva05@K@=7d85!39 zjcYfO!?12_MxX8XWW9H}{qX(+*fQx$Js3>>r$Aa;l+G5EdYdbz!eeeCUfNBn$z zWtGC8t|RpMt4VIln*O^M;IPx%-w$Vi?wX=?TrK`poMKPu%f%qtgZ~P*u=05BFM)w` z6Ek~(Ajd5tYacs#=Peb3;5GwLiTPtuot9MRHmPs(+8+R!otMl_&!WsEcJXg1m6zyo3srcSc0$z zEr3ZB^~+W(^&Ob*>+37G*U{0L6&4u=I~i=oHso6Tz5DmeT}}=)Ds481Y!-UaiL?*k zT7PE0`L`D>RnB@1+pjPg;et;d7w0(}vW|By_m{X{1N@=~;e!|4 z@HTdLvCU^|#7%O(TdpCR3c-J$QwOoo9$d=8iEm7E5n^O!HUm!#9M4J5#aR8+a*~rd zjDBZ0rb=MN!_@>hJK&=VlD1x zAdx{RrK}%~Dx&q@OULdOW(V|}`OS@0Id`p027m2lxvP^D3n9@0<_n34h*(^udBezn z+}pR~!>t7>dpS&+}vv*Apy5>BWOqlGXZFZ;=)QuOc){BcnfDjSDawi7}}(n z-*NYNqlp3|!&-+rDbQ_OURgOvE#hQh`6Ek3)xtw^kcy`r*NO9-Q7OiK7_L}DH^t<+ zMEoJJErk_JnWY<0hGrOmJm~Hl+1p>A{l+6hik>q9$0?uPHd0elPD$9KZV0NFzi6q- zo(^c%8meh&DQH+0ejj`qNc5JO-#Lsa(n!aof&arn2ENwh?a>UZK4TULaf^GRk-F+J z@N(~kR?>hGQ2Un;?8M8sxCPfhR48M+v$Hc)iS(Qvb`_DI{tY*YIH-BR`4MMfDu}^Io)BT3EDd64n+fDk*`mO9v-D3om_Wk0UO3Qe&Uwo8Jh2?5sp6>9SG-w$`^ z9WIIfOv=yya6AXbQ1X4-rdSO%W##-fmgk&}6PjTZ7!b()=uir@f>>p9z;g4iglQ}n zjUYPpyc%k*1e;(T0zHM|4ON-DhT+_%3hF&L%5|O z6HqH6E*_NvC8uLZGc8mj-4{u8Ic+4Mc1WzXVcev7=HPHtvwO4sw7Y~8$LSvOckiD` zP27wppsN_=>mMt($3q=o2yMNIg0>5MPNQS+DEKW*OY^Hi4!={X=P(TrmIIsM+d5=PnAbqf@k}OhhcrOzI(?WI3i4YJfOig6@a+e zkGUqERemE>z1%_<4#v?jtPj?B=o>VI#8W(mA-dkT1yAQlAn^u)W@vgk;;davIq-23 z2y!VVRNyv)p8Db5!a-l)G@|Hazy7Ji_A%N>RI~x?N&XbB$3ao4R-7nI8Uz-;(OyBf z-(pPRVk#nQW`w};KCAY{S{ZP$K&bU|tlD;M{5z`-)ISbPRwrr&9kvZl>-sZQ;HzP1 z+3uc$81}$c|BA#eQV%W{j84JUxpU`^jc)z|4io3^hqCa5ivt4;^h|H7UD!La_DzKN zrLLLUD@}g53Ua77LvMyvpRO>vWyKqalKO%|m)Vh!XsA5_q3EN~vGqUWtpF zSQ^NMh(;v#Wj`k=DQS=whPP)QVH=M7+|aJ*O`#q~qhxh(U|=9P_-dWcPEX+5>A-oO zP1Pq9;OaS>`iH`r`WmJm6TQTHxL-YfIwuG^peDWI(b1~2FEJ_i;c!mozF zWMyR?{q``yB9_g15d(f$Kju`sJqS=`A~`xXCEpg}X)Fqw z0>h%C+nc`wg-tErK?iBI48)<$@V6eAsndPG|nH(6)QK3;i-mcEdN| zlY@&0nnSs2#YNCZgReCfoXP5-mHqMY!p_nFb0$7N2dpMJxh@Mul1y+kf|G}XOVT5C zDzMjl4%Rrx#lD2#%C|ulB{{dPfk<~JD}T}lZNY@uWP`K^={gVxCY)Zfz z7Zh|G&2h?kl?th@4pONHw{HDeTg%POU857Z#0SxD>N?%eq2(Pb^cF%48jK`p6i|_$ zzriLiO~nFOd@}0M|8acs|02QtH@I2x8VU|D4n_n*3!BuVsJX7>C%mLjet_}y_4^lU zH$Cm;EziHYtZ6ORJXCN01AfLR8aMdjiz+@Uj!#L8f8KX|eEuKK>?>`LuZdNUDXHOU zC4JooLSmwtuuqGT90JanGV)t>dH1%2VCe9L&}O9}THeV+jHUGIRZ2wB zw)8*G7A7Il7N73x%SOrH%NH04^~Mu3JGa_F~7{hbwk3B!y^*>wNa1n z2KSi@V0qPP)O5R4G^xG*8NR;J(a3s2N@ZX_Ls^}GeklAO&h@myGJ@`jB2pfp8@kDo ztwLbalvPsFfRs5*LGI{Lxhj|xbMYk@$oUophp75xeeXQouiIH<_TZ@sQ0ey2@ zpWl}NIi@F1wBbi_P!I5U@;4;vLTGZ+6A%qsqZY|F`*d+t?o}yaz+`?Uq@`u0p7;+d zn&#xjKW~0V1*sk#78?-}T7RWeJ|#XO;Sn?KNk}77QC`PWrM$Nn)85wB)zXI6^f0BT zZ$)KrC@9K*)o0eh{yup|EP>!D13i6bTUS?0N2+K2mA!pMCFRi7gQp_R*m^-|dRa0R zceqQs({e8ML*w*V-_Dl|>HSNe`gM2pva*f*Gyws@-#H?A5?BB!SFw@~d70*`%*9N! zw7CzNW z7yzEzdtE7lo;z)3W=27h;hq==A)zj(vM!;nu5Ti?EpudqT^BFAqWoVWqr=1Ff~A7p z`GoFgJeN`SVhI&v;%E0xzlaoUYj1^hk!&x6S|V{gBF^D;b)Q%K>%2UQ0>y|9NO z2n0RKAWcQlFk8``r1~(c{Yhl66!-tY0tf$b;y)Dd7a3yUU7$n0kdZtzO8(d>ttFYH z9iN_mP7zx%!@MN0kPg^EtP>CsDeWV2JjXWB6d1O`0$(pg0Lsu}@Gh2Gf?^`sNd8f; zR}6`c*f8vF3yUpZJim|-0{k88e{FL&?Z9t|j{`ix*UPF%-4XvvQ61i(6ua1A8XDg7 za}%Nbrtat7dKO!o$&LlD4RMR3tN2Kw`!VwME{5^s(-@D65QV{y8`fF1g32 zBr1xA*I*S$UpYa;#ld*%Rc`VNRQOkVCVFkp3q}8IpKM4Oue|UQZSW6y$($B?Z>4dxGbnX|Gvkr7Lax z0B{TG6|XSlf-Zbob)G+dJU@NGoB(W-T-9RVDOCu10e8O7VN>#3is+=hla@Bs^ON}g z<%=3M4(uRZS`>rodwy}gHcq?+vdvdjMC4bRLXwCgi6;1I4)CArLHzK6vk;16EI1FP zpnL{kL$aNQubd2Fuloz1(9<8xD=dLZ0XzE^#6;jL zDFcfzqnw=F8RW1~=InfqpO~BLy3k8)GydzB=Q+2oUm$InEuzrxQhPy}z->CV@*7Qg zrm#m8YrD#S1>f(b6zpye^SsiPp=wKr3U?J+ZMH)|hASJp&v^Dm?R0M-Sf3nNW^Jx9 zU79Y3%MG9b3c;g^&RXD_32AN2t~gq8Q&F`K4yNfGaot&ES2;DB23d(_ zg~L|)k)P+UIBs0yvfxwRx(~=kk1pdIDtYX-na8Q_?Ci`NwSJ@CGOdZXV-cv#!%M{w!$LM%xCKRUA|`e04eO~n85R}> zvEbR^Qm$MqpM(?^Sp$9?s8q4bD}i9lc6l)99M1iB@wo$;;^DwV9&=s)PJ!7a<8# zJlb#+*bjBkmOI$5)?1tX`&SV!nN(CPB@C#G3$>0nKiFRIc)?|MU%YN_$@KRT8Cq3cHdiBt)SzeyW?01vTU9UTI2LraSx6pTa8e1QdjlE4vG z;dHPGum;sPZz3WhpbQW0ID2?T>HDDTI)%>|ieNpF(1`&1Fqg~mex>bNSib-V$98I* zOQK$5V|;Pfr5(F{Mi`lo0cb_7CRb7S@UZodK=;3SLMc3f}UaOs1XVoDOS`6O20w zUKy(l50f7YQG2oUSJlE*y`){tHIm12wp;KfbU_X?PmY*(&WjdBpVmOHB~|OgAup5x z%s2nG05t(!LyWSYZI6JG8y5=Wc ziOj>+nPI0VbF&lSowiUQnE2(oI)0bzuGGzP4s%6i<(BsLL`A&4UvBa5@SZJKHC`*kuf~BpJfb}@y>>8f-?M}85y2C{vH*9l5ob(BM~rwuK<`2t%^E7b2w*`Y%qHb#{2 zlBs^lspfiqdBF1`-w=-VkjDKL#53tQ0e%p~R39n&6f{70>bvh1JiNr7@yj>K&7~l9 zNK8sXyi~wdE!4xmL|}73r0FD7EGE`{VK$c6))Xu{$2;AHP~mOv+?(&IZ*HEabYV8V z>-$@5a=AM}SUNPGPQo=3o;C%I&X*#;R$f_&Esg>rXOtlUhvdNn=^?*F+DmMwlJn(I z!kPj0L>YC$3yyC!0Q-P!oZEEt`?)mfuxV5FP=u45z8J~&wiWSDaTctf{vAlL29gre^IiJRm^8}Ab7Z8%%6M!KhGl{Xxr}<7%WL|M8`=(vDI~Iv zbS^7J%yxeNg`|ON)N4zxH)NKRlj9Tqs@#lNhFuJwbArkqlP+PuY)tvf4$y~o$8()z z##38M%hWtz4dKd0ER*wpNP$Akw))kx$A6dYf2xqWdW8(mi;duHuLY#XW@w5+y*pbX zn7e))@WjZxgxpE!Rl1P2GHiL#y$h|lL;ofp5%5Y>m)4Yruz&uMi&U%0c1#DFfM@*~d8qBD==t`--Z#2WzFEGlUq{z?q2d~N zRGlXGUZ0PTk4oCmdg(X%nK~Ga%BZ(ZZSAG%_hbG5)__9%MVrhXxsy7d4IQTZr*DTJV8-e=N&63?2=Y|pd>oQletK1 zQ8>l?A+J>TB4H%)zqsJ{q4cF%@}4~W>TCR%C8g=Kd2T^>U+(Q4=9(Au_Y^+{r0Z7L zto}&hkUbONPxlo{8r<#-KO}G{1sQF81Sk&*6MWh8T+I&k!CciSpi)+0wk%GLu_<~!Z&(h3aXjW6G3q!jZIzDuh&{>YdV?jzK zty%9r$_Q2v@{4gj+(vLC#XXVO9%OhiKhQ*5a^jkbIU|= zJLmeF9G5iB=6NtaCS7p??7P^l=fx&j*@Ir^KK>-iRiCI@$&CFCst^E>T#^eoWI#rl7?zq^76GN-jrQcy@3 zXbj-9SV1W69~~8LAti?Mv@TTMelCRCS?pIox|zr#r0^-mtS9jjzLf>(UAX_D>B^nj zm?Bc3-$iZ{%Mc1Wt6Ex$;&O`_JYG3bHXEWJzNc>=&zmIIO1-J?R^)ctUk4;f!GZrL zQ-oGw+k#v<^z}A$HNH3?dk?zSVS{;?eTsen=uFTJK z`y6yFrhI)qR9iNDNY9GM$by1+e3QI<+Z7X&d8L+n{VecX9cEOdv-O8Q>@3oB%94WSM_I9aEqhLL4l|1Z4y?ZA!i?S zDy`2c!B9Ch+`g@<2^AG2O8m+LcgK5Q55kQ}m!sg4xFK+Q<2~|qB=jymUbt+K+y%@6 zOsdkxCwk#yYCzGb=e;|Jf4i5Dd%1y{_^^PR#cs{qXm?$hek89WEl-?X{Z~TcHlC;p zo$~46m6tOQC@3f(yaBbqhd|#r{WxEg`uisoUP#loRfEE{%h2}x7JP}+6*;86_cJwI zB~_M;%LOjOFW1zNAEz7D%2`x;Cj8Fm&}yb>6)2yox)hlx#ScNZ8#N{Wj&1^X4ywq7 zbM~M894&3ES4Sz{10m(HQOZwE)7bI58P%*fp~9+1UzJPd$lU`T+@0n=!Zk3wYUBM< zj(O%5nvdRQhUm&j)%}M?y~zo#?`{PcbD1o+uEzgPOL#Y34(0+ttHNxa8}~75=+x3Y zG{aU4{r2!_>DC%}sui9ak5q{AOZhp3S!ZWGL7bD`H{=SKseY4yTUn=QBj3{egg-JQ zsbKF>@Yj-(KV&aunYI=MBqZEVz$IEF8n#vI#j{+Q{gc-%JkYsxp`)v#-hQ~P;sjkgZxHpm(Ga@aAm?i) zMDG8x#Z4_%`1}PQWt>kX6LHgaKBKqI8fT*5x zDE5qy(Q?{B=8}^Uva^nkMV7irdXkTc+Lvot9o=a0y+2eI7aNa_jcUbT%+qniG!dgZQ(QoV5+Zy${=f(0ebKV&GOFhL~Hm;+$weQYo0kM-HxfZ1qxM&btddTJR4R; z&Lkca+$W?gXnN$l(a?0QhJw+v$?xSpQGy^vwVuO_*6kn&d5;M{eG)ZylvUyF-&c)$ zaOhct>R+6Tb4?LqAh{SVc-9ed2f?Rcn2kxDUs(9e{MUv!mA1F`8=eMB2xH63Qvxmu zbD^)d{fPYYzApb?+`p|^=SthwbaM!&_?F4|gQr$NvisUf3dD9@qF%m!)%nc5M#i4P-8}uQ_{jTv zSJBU+(?5SuS9d?UCPy-9VUagIC7blAbGvwPqjw2VM1)!uGtS&giCaLiM<$7CH%j3{p zu>r(#zR|`@o&4M>!x=7woFuEq(2yKD02C&d*?9CBU1+?4!5Szeu((Ojrg?>cU!$Yy z@2Sz`GA&-$=}CGV%AD8lPksUF(T;X&BaNVgr2N-Ew+QjXW&-{4FHB-_=4|ZAV{r#mK_{HU_hFX&Ip0jid<;O#4oZ9&oJl!ZQ1%*;3GCErt+En14 zn_Ee9*1S3?%N5Ta%G&`oE2wJuD{T`reNS6~!Z%fXj-#i!DkUxzf>__;BdB@*^44S zKfjmqItn@39wd}mK|Yr`p|BLBq_1!_pyTnc)>hvWp^(ABk4Q%}U>dT$~0 zUiW^+sFjnDZ|v=v4dy*M>w&ag;&sN1i)s2&f{QO{YGIz;+G{F@yMysI8&jh6sUN0e zw{aEybp&?zGHUkM-cQsxw^5v2OrxH^wwY-rHRRDc+HTdFOfg7#^i<T8{~oe z8Wew>4rfn8;5Ik+cr!XaKUPuAL(~a0TahoDN$&&zk9hiG^z>-7Kfr)R;rRGH=qz+c zZa#SWm7fwU8561*f?;2LYSkt^3AsDIh7(d(#IR9Ou-eixd`pj8L{eT$El$15f^s&BooC;ad8 z7=dP4hO{S~K{;dbdt^8FY5{d^kuj5l`fYq$Xm;6ng2XJ{=+soXsC#_3!|qMnJqmk{ zUKj(oaEU54`S3C>VGSet<>oLnitnAUWqw}wRZcft2)SyIX9nZWQl&`*|IzVe1Gj7U zrI>1cS79ocmM#XY=8HQ3CmR$fGGIRvweiS&JI{|Dtq@aeN{ZW4YA;ysa;v&a7Np9ai+7fFQBSbK@{b=MBMI6Jh*o+_n#yT7^yu<~A> z8;_}#^DG0=_R8wCj?k(`4rtA#ukK6-3M!t)EAY{~RXZJEF^kIzF7JQ1og4iayv@k# zv|oA68~;Ard+7f!FVF%;PNc_^|C`(G*tS@H%gc;&Z1Rv`r8y^w1rTew?L)!LX4%g2 zv0>}$3FkdB%B=C8ws&3nDVdp-_FHTM(Y8Hz zRRKUP%Pr^F>gp+-oShGhmbw!zsL3ZgHLgTARX=(U3Ycy6-RYI-Y2gEZGLaPJp4y_t zqx{P_U+?WEE<**{%tW`vP2Mgf{!eOZX8lXyV!lPo)(}Jp71S1)#q4$!xcYp!3-j|6 zsuUMVI5;?DW1j6CA6TlXjWjnmySd?VCh|s22{>Xr6wa`!%CmOoQ1&8eQ=!V zyMkyqzpGx5o}Lbfx)=~#QuAeuE4Hb4v8zec5iC1Z+_J{aZF)Nu>+|0~Ok}Bi^ z805>@#X|z?9$%v5GE!7n0ia(IxdYSLgG1!u9SOthYZi*52Nv*ocE}l0Y&bgO`F3uf z_VNC>La8Dma?#O5@38WvOXrAOzHZw+yzu3Xy9}?g;*pZVEk!amXQ$33b>bsV+j8d- z&@jt*Zk^p>I$X2>#WIFDk@}Ovoumqq%F>9Z4ork!>+0*%2kYewk$OpMdO=~p9-pfT z9b(_Klw0UYYJ!|-zlmjUvEQ%pR(bW5!P6a3-l#|B7Zz4RCS|KjD`rR1FL=7|F`^u{ zc`kGVMidhBQ7gE#ZT93^0{+wY+wqP$BH%nle+AvU&w@d&JcxU?ha5F<1;@Ed>W=a$ zbDLe2Abu@KcJgIgR`He2TJMBAKbMz`A+ng?{Lk}#5i#g)q4<1cMad3NQ&9JKom#Hc z<57n~BO{K-^3$iklKcvn{LYU;0!o*Z=e?s4lm#Ce(rdWXEUtM{l*8?%D5s!c?WJq? zu+p8zF}mMRpFWlU@eFE*Ag68U$`y-ftgrWIRpmGq*LhDcJ>S$rN6MJ+zFuS8ZL9vK zp!^Y5Z;pCb#6_Y3P*3_x1bEWRiDx!DD0eU(9qQZf9<3eji=eaWr zMgKfIFkhidvgX%zdgjs4ASi=Xe5`Sl`7J4l5`BD^R`xmxhtKKn?R!0f*?Jw3JuGyg zhX#Bx(49=X({Djh9H`W3DvfQYDLxu=#-Nt%?_vLt!TVoC7#SO)Lkt-}6MIZsj_nqH z)lK`qW!Uw9b_lVr{J+^1{O>&&!N#fT^kNEhI}pe2uJu{MAdts_;4q32ux%M>VrE6W zQ4$^-9nAtU892!aG{Nfd6Kw6|0uJ{`iE`uP-=VZI00Ci&c}7R~Z~=O?&vnyVgtF7p z3}D_}NUx+b$5Se+{^1Uuax?4%;nb+WwM_7sIKdrKtE;E?dsBG{yj1pG_(@SC^|ZC^ z5dc8`SL@zA@&RUdS@6z%LXAb?km^^t6OxGRFX=5XA(*rR10tPr=uE<(V{lT7UaPcS z`NcV&XU~h~(A7mx3gdE_{f6n)XV43Sp!4EI+_Rm%)AZn4Y{MUiUMK(`B^PTizYZcq z&=797pIoBc8&68@(rEAOR@g=9F4HnodMQ&_w>Usus zjtm6q!E9sNnpl{u(idWaeeVygIa$(9D%Wl)V;(G<)IMM>!r+1smIJ*ZX=AQ*kv}L! zp@-FfJzl}r+28zKQC8T84_8Cxu%o7N3^NUdR(>YY?v0YesK(Cd!=&hF^>mZoUUq|w zgq*jFNhBohGxzUDxQ7`oa40J&Gg>PHZE0Cr5=HVKAIRdn^YqCphjVT)&&3~c7m*3P zOIlvA*H&SB_NAfd^--+4! z#XHim;tZ3~sw7x~zt)A&Uyv3U%q0c??1;~8t?k<5pIEG|cQ#e^bPL%Rb>9oXL*3*Q zEa_3aHY*9fs)Z>5AQpV%BE={pl+UQ~6o>u`S<`!O&Y=Qz2()Zuzf|P)Hik!kYwet$ zH()13R!{N4GdSJjq)LOq(|nGn?s~D6wa1Gi5>xn{niZ0z(886D-H$<|c^E%``3zGz zlyU3YL@LpfX88BREfuglPJrK%oFt>t`X8^5paN<%29UTBNF--dBmXM=~d zlx5mN>{8b(|51b7`A4AC`(xF^%+R#1nRvsOgyG-EMfu|u*wh@WOA5!-z#D5*$qw}W zLTG5HZ^ZAqYDE~@D>sgP%f<3`$OkezR@TIZY1NKDM_#LCYwRqgvlBU=yrrd~HbSLE zMMdqb^j8OE7QU2%RAi=>smx{R%ZLYU{)Y_^^Q6#-#XKun8cksAz%s3HLX&1y{vo?$ z_**fcimfQHQZk$&?|AGhewcLYU(}cXPso-3cMm4ODs<8=HM#(1FU_Y`L`kW>`*|(6 zKAjz`RpjkdDi01TTZ){ZAsImrgf{EbUS&D6iC@2xzfIkdYOC0J!dPL2(pMNzPezw^ zWm*z3d5enN`%CmaDhf<^37{f?KHlRF?dd6KmhC-zNJmLo-`yRryOjbyL};68W&9&! zbQ^vF+W*eR$QWiOM9Mn?l6yj>05AR5{S}k*@E$ zVY($bpM$oJ$q=xaY)poDc$UugDbPiy&JIx1R1p=WM)2q^?P`1!dIX(XUdzQmX=@K> z>~cUci(jsUmY+B~?3xIAap8PW#)R~`#BxG725$bXCW;z#@>#lCd?=z{z;5>f!TX7<-Cq_-H|L$d#4d_ zpUEV|{q+VjsLFD4`hPlBzz|c*z+)<7|j@->TMvV@Dy0z@r1oT`hZ9{=vaKC zD&L))7@rz`llP^=*-y&I-PNJc3I9c(m64LfbMmF$a&V7Cpe_-@K5-icAItX?aP{eO z?oV{Oo{rf*eM;qzr!hRNdON7+=mt6ZIZf{qE+(xFv&@sX2TM!XK`%Ain?q&xj&>qc zO1C+j4kkAG2p+_cK2@Oh58FSiPBGLO__WMIr|G8`U??dVl(KR4Gn@z8Nzj%NiOgsH z@|a9%EItu-r+lVDYl4s14n4m{3=Cu9w>#cnu)KRWDNbL8la(ux8m>9)3_^z!o4GG5 zPZ$?B)j-K&vz?}8tosF*8dkmS;V9ZL*meI;n~#tGq3zn_w!c5*y~@g=*fl`$%+1;7 z4l(7E>Cb{~Ae>3Lde!}QZ&{h$&pRiIk@xRkjjASlnJxAY6DutM3|rdT(>Y^hC$%xI zePidG9Y4p-xr^?w1Ht9$gmZlghuG8EL8+%z^Du$k%rHK8Z-raaVmj5?!6wPNU?=Zp zV%|IjF#*GMHSiilym_;YH{{L6#%7q2nZ$VdL_@2ry}A94?4=GXNh2}l&y9+Jmn*Y= zMf6V$i@65?98G6l2`78+U6aGTl{dc&VL3f6NCw3Of4I1@`UBT;FE1Wft8aKZpKuS6 zO^J5MS9pJ)4vv6h;&OlnF0QwF?T5Ktjnd|{c|c{(@aKncHh*S(vNT6WPe*VSLH{as z-2;0f%B!t+B{e1lo+pj1E z0g%6@n!l-5B>C5`bo7Z6m$?IQe%OYjaQIx-P&&KoT}+5SSA) zSkH1*dEmR;?B25EO#e=*DH9FY5c;k3@23hNt#6RCm0DH$B4)dba^qQL3 zk2P;$=5GU=u*cpF#QOH8*2Sbu8#p06uEGfs&j-^CI1HXH`aQy}+TN<`i#m7@`aCEP z3s^#(Sa>;|W6y-rH2ik^o)a{Dk2i<5h)75l33djl86&yTEVPP?Z7nU; zRx`MiX}z5K|MAl80;{UjwI(auN*NG{nVGr5x#oBLE{DgBK>kzOKUE%Q@a0#E%$-;o zJ0pK7X zAU|sY*$NJMEHy7TB8-YUHEXOa^sN&_QOhgF-xqO(Qs;Y_qjeX==sJ z&EC=4s&|(Z|AQ>?SpcAMN~rNoU0?#xsjzsd@dO&~j28bp#EfPGDb%(Ja$@GyHajpQ zUbO~Qf8eK*?yHB=CLkn}YFmnok~wyHqJ;klo7DYqY;@vr0)7HSH-pt{P5vY~xv6_V zfw8lr3Qecu>xdO7mze&9NH>IFTc@jUqX%Lj7;}EI?n-;!L-@|!V8as#6#_`huajW{ zKk*(gI_}JCo4%TBKafINw6(VvEf%J~?uFr1v2j*bq8y?M!~D^rYHTi0^N%XZd#*y(o_ zds7urK$Hbv7Hs&KW9$Mfz@+(-pwkF5xzWifzJi!k74D4D&2XTQ^v2uC=FDF*NH35~ z`g2T;)YPa!@$)u!=$q+pwM84!(*r9651t}asDhBw>)_fw*50pCQQ}HuCNi_^!0k~j zFqW~fz$5GeC5qMG^`G_LH>^zNqoaaJ$?lR86KHE2|I&CGRxmK7YXlJn6e;~rP#7?X zL~@{BgQbG}HX*@-jO=Vt%8AiPmF$rRB=-`svN$aEznV3DSsl&iuRh8>z7YKF+w{~O zpp>=3E5W&f*1ghs(n#iW_!-sNJtxH+2mBlGi@fQhwWR#EL${9xFi3Ts>Sp zHnYB6?gyZ0g0XvDaWeV-VNah8_QPh^Q5Td_!moBiVxl#&p0szkM6f$|wj-vF0{U$&;td_dXdFuc zx4C*E&Vd~g%7y+QcAX+QB_%R8cJj0?Si|Bdhg2rrn8so=GzE@6&(wiR`y+6q!x?%N zB`Xg|J*y8k1w|}Gu5^#tB2z|%|0A=L-e4yHd4Gv96LIVIf642HA+KY1_fy%&;uvv4 z<3CXpeTS(p#~2>qb{U3PHve#0uFEt3sjF*P^^I(lVDg=rRA1UWaU|F@VS))u-N&8+ z)y4+uywsPtIBtDbL9g<2aMve8L zK6w@9o$`Ouy}A#*9Fr|U{YYg|rgLq*7|c6;Ma91D9V>`d4X?ly%f+DANMvuRR}ebL zYFGTzO?oD+$9F7*rwW9%E!}krg{LfDXcs1Zv+(m$n@Vw@7ra?Y{`+@Pk=X?|N#s_p z^eu971T(F^u`#B^`Wlzn%=>GdFB3CfIqaqWkLjXrblZ2{cuJV^o+z}ETf0X~MdQo7 zHc`r5jog@->(qSVSR{T#G7-!6bJFoM5wpfC#64r`Se{mcOa|g5y89^5;r}@a(mSeh zztcn$qlCvn8NB)U&Q|@Mt-8CLL*7~UZ(57x@#ivDRR`5Q8SS^5yEGkfvIMgXz8)?K z9c35`Y8?+q?ti*}Z-J)h=7Bu_xY)i;=miZ(Di( z=BQT|pLHu3F|^Ipb_RY9h~jlcFdiEniCJr*1DGGPIh;<-a1`nfyb={v=Om=hO7`Tj z5wz*p_~Xg(GsTINb_{=lx1UpFHP?Dxvo3Z?A@j?nnOUOn2}Kp*56>B!` zsjfR&GW|_^S-iwb=VO=UWS6kJ4r*z1lNRpsZ&0#zgW$pY{JDj)mRNo9b|iRt z2ry}bGF!VPTp?X_Xoy#f)aGs+>xHz$^1BMLFBc0D68wEM?gN+WEgT6EA>I{xM<4bR#h zrO;gLFF(N$75S>E+IY*rX&B5ZH*xDa#tLR__6gz+>-}MVF#P*(Y3{N$h7eI7WA2Am zGtZju7Ju#3*{GCYWn`KwbEyPo${^%ee%!qmClw_;!(q;nN|$1)lc8slXsRY2Q;_@F zbRj7sMU6ON?4$Lo>U8MPeeVyN*>oUrh zh!_~d9U?o!R6BP#Vir*>@;Q-PKVd52-p%V3AC_|LL_CWpALDcHl@s*kUAPw>ra@N_tAB36Dyp`r z*32^Lk%^3ha;M+Zv^yl<_l{k3_n~ zLrpg7Z4r8J4(+3?Ll*h&Wu&**b-HK`m#~-B4F3;xZy6M4*S+~BAwclp2`<4cxI^&Z zjdp?tZ#uX;!5u=7;O-jSA-KCsSWQ#Gef)zo}SrRpwv_wIY|z4lty zx_&i3#||>V`4Nk;Kl9&7e;v$DA*&p%Z1zLwi=u!I2|%467<% zgNcD`Ym5EeA`mY?fWlOmiClaSSI+KSQ7XQP2};QYFW`!eZ;}f*S8F;RE&(!o65^|i zyhH7!&eA3IA{5{tWE}zWsADofZ*6Kr3y<^WtH~|k0Oqnky+TzbMKZk^e>}H6lc#Hm zFN~QQZZGDq-)0}PZDX>yC?mUd!TY+HTQ>>-(jJw*o;^Afr(P~*R(7_W+})mE{QWd& zb1Jg1`tS&F-*CrnYdAD`r@IeFPRX;g9a(B|ZH zssu|DNI|Ao8LDahT8zL0OUrubtH-ujbUuz<>uZHjC)eA8ME-%Cu<_iK4-tLjAy~;^eB6mUemddK2^{c(-0G%sJo9Ft7n;$9^z>q6Xfzj zW}RDy`UHQ>wHca33D1ZG7Uc{&Q(TQ$ieNHjdl-dk>2lpe)>77nbHeh zfWMK;ig*wQII3p)*s3r_&b@8$KL1t4ClG_8llc0H%2u9$WK|__k&t{lTEtgv2`iSB zI*FRf$;wILvmKp2ve#EG&+)IXtNRLch7i{Ag_Lp$w=CZRT78U^j;>AyTH3gTclNt8 zCAI<8Kv%>_oBJccNReICk^idmbAAFhzc3pOg?Uk_I?6!ERF~eJDf?w{C>pezT(%Ov z@fPy)^Y4NpS}9&LQA?EDT+GB@H8*lIo>%(FA>yU%qt(ra!|momhh+Q(sm1p!pAAV3 zC9gM!iH#qOXxN?0T2=&8qrW!Xiz8c3sF|nC{9vP^H1DJu5Ii!ZKOT7_5q|tr?3P9Z zX{=StCz)?@GRxBj^>h1d_sIlYV6+||_rwxM`eU~{R{v~J*znlrDz9u~F^1MFBKNJB zDjIe9a){^OWlMB>EsubYLu*LU4JpvYCc zCIRFVGYGca9%Q8Uql)!}>4ICl-K5|N088#EiKT#i_m#w`=j0kuRYMKxzeF28M;<&5 z>FHyjEg4bFx|Mjs>yote$@!I$6wZgs8NGcM*^q65KNcMezPvR5`gWqArj*-BSvw%| zJx9@0MG(2WQy!uTG;gFfDYXNA)UlxOq@{O=f`4C%YQvn5m_lcqLYwgY0D&3R4ef^u zAy4h_85=ISM#2p8xOlOmemWaa)A|*qSAazb-i@SvQH51>G0Bt z$2D0r(Lh^X$`;L~JBg8FGP(>~x--iV@4mcS*RbbTv#bZP`IUsL%4Ss4n<1VXvdak{ zWe0t1le}Va>ta2@L4>xK)C^GsJPOmtxp!J*i?;?oBE@0xedhun-Fshpo>0mGSl>;w zn)g$OXKw7kvGR$W?SsOYrL(IZ#1(tl_q~F4_PIsyqUKy>rj^4qfA)Qr1pJ@o!7vw2 zPBZPU5noT$YDFi1#VGb|>Xck6*vHdOKfoEK9aW@0IWu7e&U25<3zRiSs!FBhk+{u=>}Hk${Im}& zsw2NyJ2?w#CTr40sbnlCJrctfkB_)rE6&c1?)ASn6wC@g+(-H1KEaP&ru5j%{mm3# zY}2{LJt-d%_PL-D?QpEgqrKHh>yRY1EvOmoPNEn(`^!PFaKCKPe9l;q3XtO`Mhf(w zlJZWsq&lnr0CFc0JM5)=u!aj4=Yrs%Up~1G$Vf}u#6a7?LR;v5TTvs0#;-_~ zxOf$Dm6c*z6`sK%2K@peTv@&b0#H_(c^cIHudq9d@(AYa`@pWZHTN^0evw4k40NfG zPn-n{ixZFRlTV>T{rqeAEAgJ7OiS9Y|UsYavT?p4(^-vxm(WQ?#W5znG}XmNPwmpjH<=|C zcP>nNJ3TZMvHh*yqY+!h&jDCXZd{DoMQq~9!@rXU{HrJ1U6}dBss*#C;sT7tVIxh#+Ay36sRcohS?B4$Ua67%3^X{?^R^N zsqns?fZ`Xxsl;Vk{1&>5UR)e<=MEIf{onK4Kur^tuha7DS} zg~<1228%{e`ztS*Zl);qRVFt}A5DME>wh#4b@%IosmAKMQd}1_Z2JK<;y54(+(NC| z^bf-tM^Pt5!6N0FoCGBaPTUqU8zY~Reznb*;r>3&@VN8h-AXutY;!bZVn5iGB4(tI zR{^Bt7t7vDv7@?~-d%FKr!?#820n zKPU>4J#wVo{=9O->OM_k{H7-9%S6U zxG}qT=}H*iNEtPSXT6K=bYr{)!y4kr?I~lLa)sSbMo|<78Pi(WU}L^2^-5l1R@{4o z+78@RSvHQl+Y=zqr+t}uTTBtOEWi3Aex2TF0j&Ie)yHrd)wdZu5;Zy1bH7sEXj}e# z5Fig2G;GskmZ!wBfz*ALz17K8@a=O7Gp`grajU@Lg!r76_G8}rxg^K{$d3E%ip){` zd4CmhZr#{1ICM)BzPgr;n6041VgF#ngd>-UWZs$G@rcE2^Q9^pQHQ1{*~oBam+#H? z8BF#kxuA{x37=Y9#ntit#Vw!akF>jJ5F@Rr6k0}lbtQe2z`l$T>bRhx9N&)5p_1b> zBU5uqSK}@5t?4N@VGFMIKJs*C0Fff=kU!g_S%Ct+oq^cWCM-9 zqRM)#806u;{9DF|z0ZV1WGA!;>ot!aq~R!v(I%XuuOpFqqUPshzu+k1)NeHNHurjo zf*+}n%eQ+wKD(@HM%OYJ7H*z4!u`)YcA-Mxwt{li#7uKO0&;4J?W*iO#|*F^?{;hN z{b_I|%ljBqT|$=ufX;V#b!8-U5k=ZMs6SgOd1LXLhuM8M8>y+Yo|e4buw0%regg&7 zLEZ%U_vQ^;wkF5VagU5osv61~?TiVj24L!BB|)8VDn2a0R)Blp5;}bFzLN-&zd!%` zo%9-`gOg>OMIvMqAh2IkU}mu|jDWXndCOe>Cq9`Aej!8=TgN}T?(XBoA2>~v5_DPd@?Ceq=$%u;bQV#P!=bT-B1|GX4C+O>O7M~0@5@2LI>l^zO_ z9Sl(%jF9hTpt47zsm1l=)1&47R4+({5Dng8BJ7wpr2&RVY zAez|oe54+O93?dmZeQ|EtSi`*GDU@$O)uV)=ZWB^@7q84Ex+bdigV{1#N00~lZ z((|hXU=gI+4)gQoE*#lIXETFD@Tb`#K4<^tyGos=XwcjFPG_D1YEcQtm=*a?Eh{Obi8A`kbNrK1nz0u3M#8izAXy zHiT%a;{q^xoMnP}2oNOytxWldPWAgG)75V}wO9=WQ? zqFSEKyltC|_L}Dn^!Po98CYe>vJ(BMZJArDvvS6Cs3zGuH*r&*XGW(RTSyz0RpZ}i z(dZnkCy!=3Ku-rqqIAe|vn#So@9$$_1n*k0z~<1y-5#XjM3BL~aA$2K=Lq+zLj_~4Pq>)c z++<9u7OWz6S)8djzDTGvKjasbYE~59+;{G!p2-hhP46J%ea(5Dls6}m%z_pgoRl8W z95#TaI!i>|*o0()3kxFuNPX88l10c8MH1uS#7jC!&&SP?_M5ysLKrtJ*3IYyN2%(d8a^Tp>5biF5l|uS-Kl?SMAIJq%aYk0{iK+PQr|a&vgXyjsRj%<(V?nZr z#4^SQ$rlBuZmPI#YRkY;nMa|#D?(BwrJ=5O_8z0QU^v*4i2hDRI%e~+MbKm`U&^Jr z4wicquQ3ZeJS-S4d`4T6G{=6C0V5kvVx>S zNLCaMEJFTZg!9L!8p!L3^$Xd$h%v|$Ol7TpWsuTV(?URS=XY#F))SA7_~?Po(&a zk?f(XUmV$dgOphaxSs?04Irz;VdyYS<7bnYXo+n8VkWKtiQWWQNL(X@%qCy_lt*i= z8h9gW8$9Lh^CyY}yHsGc-s9EMXb9B>3;mL6SOjW34<8Q)GMyq-XifoG@S`|paUw7C zCUxT5iL}dLAUN{^6THi|w0jJ%1V~)-H2mPM*3cKnbkvqtE6hhLKpN?B+E{KQ&0=L< zUI*yx#BE-fDyg52pD8>6jG8@%znR-h%ux_8Rh0PCD}<3Z02+$d{F94U@^uF>_k3(o z2z*-+ea&FJPZ42}Ls%jg9eE?V*H>Fnijv^i&1w=26HU8F?8 zz+VlT?F!^m)y)=@y6`B5i2gM%0XC5Q{@I~n3s?|~VXdGDKZV-rPBdxIUO?3_@o1?*26 z&%%;48#?-)u%inL#j2+1Oq>3uMoT}UmSMsxvj7`{366gTvjb$Rw-n4`W3{iBn%+qg zxV5pv8I;dDJJ%&R00uMvRH)THsi>_KG9Q&(+)QT&u3~MDC)>G#7526>SnlwzUgyz1 zSDf>ZvG2ZEbsI>2*wk@}{p*&Hv5NJG4sHK?e**>?Et_51MYvtJht?PE)-$0j5VS z3&<2cK|g!y++JrqGwLN4mv=_0c|u=6ss9-XpYW-IhwC6E-Sz=%BCcP*qzIGOb8%lS z*H+B^5jXp>o~YFgM~e#?iuBL(%J+hk;Y&|(3^JoChj&?4j@GlZd9DA=rX*jH`@Z;8IsD84mwsfNAj{u$t`$vDJCa?kK6`kN^}x8(6qiyvUG6i!(H z?Pj>9vw(R@sZ_-B|0{dx3Q$is<*a(hE!oSSv}0Nk7+4Vu6s-TJvhNwM!|8r8|JBC&dcETT&*2_R%dJgqp)4FA zl>szd14Tva6P0*_1Qkqlu1q@BK+y)6uilwb$uLsx9O+Ddz(jooQ)%Lhre=TFT(IUr zfqkM_I|We4kwhmwJ(w9y=C96me2Ve>rmSqEr6pR3Cl6E;uW2trCz%ChmzfoQYJwIj zq~jZsiv=sg4y1}k` zlF9lcqJx(A?5BmJx+ACEiYeo7sOPW$Ee}(7NdnAS3xFY%%sae$xay5BBPB$nGh12&N; z3nLw+;Mk=Tna#u1awU-LL{i%3$yj0zVDJN|r?8y!WV;V$#kczlpD42&kAySg zMc{5hC!tF=fT|S=EdV$hhb)WHtCmalnRGFOkhG|!a#MqXDc;e6!TGD_Y5C%#K{(Ds z&94hmJsqALtMM2X3V&X6cm=#7@?fyZx2MH+ge#VfvKm)bS~}#Rkat8_k{cWZeOVd_ zpTz)&%-BJAvG^1nZx|WvnDgE3sGN!31omjN_Vtl_>ixB~ulsBP_b#`h(d5Bva5SGCjYgKp7q=1<2*N+hu~@l%m{|mK7tmMW zv*_mD2njay^`m$!Rob=YYKt|Um)H6Ge{+`fUlz47HQfod^88dc7S8xj^?8O;=Or*` zNCCauWM^nY! ztOE7hm3L485%08CBcVV8!Rns#@#o`nz;^*y8bAF>@0$4)yP1a9)8h6E4})f% zCHAqfwKu9StT|uhX#;O?rdN-TRQNb*nEmAetSKoLP^bWm=-l{_-z7n)KmOH7gETb@ zzhZ+Oqyc3ZoVifz>fNIXp zPQmo*eBSBXXAd=5MN518DqePvUw0!?vvc)}!14vy<+PScjN_A&NJwbH9;b}1@m?%a zs2e3>fb3^=W9|3Pxbx!^6=26FCN`Jx1t{uwfK3^}H}lDiuRv?v^H;<&x6cMj+%wLp z#{7l@21Zy)4&a#k%tWB1wW7_hY6(;rT^4&FUkU(}|{gE?N91GZTZs zA;bXR5{(948n7LA6~H44yK}+ul>V=_3<-_7S&iXTer<{D^C-s`FaAmaSs+vPJ3TFE zbB~RQ?4WFa0{aU#76BP6Tr@##Vo3t1g2ch@#XNu=*r?fA$WLxwOne|bKeywrv`hw! z62M?6J-?{WAD9kj9qSrfga^jb3oNX zDzB4*AhCYv5^}i(#GGcv;1#j`mjxuxGFVU)AS~dH-;9mX>|l!gZIEJa5rBgAsfr91 ztbx|zivt)y@)z`-8Z-dB+GSt^{{E~6`!R4+Vxs*B19S-Qe`p{*2L^`WW#Md&@eY*4 z8zRNeuST|&j5{#75@%8~n*a{Y5O3hF&HrUy@pFvx3C}d2&g^qdj1JQ|F<1HfqtRrZj@5A#F_};D3{H>G^&!8m*)qX-D@Ir z|Etn>M4}BechfGi0hPYpFii|n!H}>JFrew?WZj)zP)*Q+{|t}#qojJ5%`G(;>Gi!$#Zx82v}Gze#n3_z2|~dwiJ?^Xr=qd zx0sDhOcLJ_GNlM>YHEi3j~X(ZHa>euApmdd2SNn8pQHEpb?ksJawb>vf6|bt4Uc(0 zOa6XV6I6x7q^ElU5(U@s5fDq^S761!?jj(@0Ii;Z9Vb^xs_l05(8$^GI-=1n&T&O-Ao%>+x%cs{M4U6KDA0KrWfhG6R%2|Pdevc|?6)V7^M-j^V=X^KD zJLB@r|9|veQ8v0?JAGlS?$XHU6=_EbE9VD|%U;|l+ac!is|GKg^!v8)sI{Izur*TxhvQ)81u0*c)OPB0MN&sVBT}?>^^AY@Wd*%hr8~gLG z8_FSM6~OD#81@m~E+D6|+cyF~=BZ-apnsDqLD@+~J&Tu$s|P7JxX)WaDgr4g`Qy^4 z3S{uUk~yxqf7`pnm%s`GN2$@|iN%as?z|KJrtO~AEfU_$7pKvs6$Y4%P{S;KfD+HJJb|O1G&CGiO9a};Lm!rs)N6fjU2q1QBp2Rc(bDE-`^e^ z9IZ@(O!6OxllQOAl)>Qf>B-6QaZEST3ikT?+I64(G*5N_k$7Gz$jKxage;hEy3hPl z?P46>^dkpjQj@H%84#%DbV*WEOLj>joa6R?7SquYiW#o3v4QL{Bnlny*5TM~+ZJaQ zOGrnyAyBFT{Y02btV&9(kxHzokE~hxSTMFY)SS~Y@(6#bIB3Wz-2-+`Vvo)40iEze zYSZ0|&pK#;DE>2pHn!r*;j+ZDAqAds+pBN(&z>_q1sUz< zqb5huQgXpr!zftv6ZhFKYx!Bjb_+Jy=A{F)sM(-dSiJL5;??4Se?Uxb-X9Q-czR%? z+7rr*v=yUaKH{sKK2Ik8n(%qdm)PBJN#?ND=Qc416Qq2l#5ul?5=B`d)-dLA8+CVC zXsh~uh{)_(Vkg8uzGdp0rxkblZSwNe@b8gU%YmB;DijIHIxa=P9+|Ll_p$!zuZY~* zT8zDhxa8{k2(${I1dSDc-zbfps}Yva&aG1*2&nuNn^|ca#~M>s^ykm#?RR|`0^x;V zO9%*3AgG((r)myDvK2y`7SE4i!n<*dkrpA>cp0PqhBRs}G3!`u>)~F@?LM>jRSbQB zS`69-`Xin4?=*X5^JtI;qnq5u#1u;A{Ve3;8}5FVKV%U=IFWWMGELa_Rnj8EdLV=RpPfrLU=%YmRJuiJ!^T@>8YXlZxdQ`n@9%Q9uZd=)1?Vu7NBk42}b zxuof>1Y4{;7fCuDLY0F#cI5D%6>zb(u-i7-T`tJcVx|_JT|0^&DLZnZ!64vLv*tjn z&P97Ntr|M$A{WK)h%@vFcvbryh++|do_e@PZ$70*@MDFDW}Y0w=7eWISQRYE?PUVb zmfItG)-$?xiPAo~?GJ}a)4Nz$OVyGy|1Z%OhTtp%8PG_EHu@F0((&zNaN#B52rGC?_ncf{q7a~TVuB4}LFIfLEaGg#s#u3yq-Fa`i6TdBO(@0%? znoik${8M<Ix0HxQmbXq>B%J9T2Y z!X6qC78DWE+AxCm*u)lVZyh-&sSK%@eT|e(f+Fc`;q#>CdV^n2k=%5Ql6t>0!66{- zzH*WFr%e`nm)t)pRRa+zt%IlJkZTU>d`n#1*WmYGrJ@5kysA&w7cqsQU-BNW+al?^ z&(bxkDF)zQGiZ18;WTgu^L+gq-J_o)erZrBAupZ3))E54+d$Q6T*#g@)hl*CwD-!? zS@a79`PS-4a0icudA+tI1D z5$;<5v9IGzi%tB`kYi!{&>%K9`EZ@Wxcqm0IZU8{M9BB4qi7BWlrX_rL=585*tAa~ ziXzt1WdkN{Xof)VxA~ecfxlTG|NO!Va-N4(mu^a2`>1BtYshbIrnSMG6eWb|ToXfz zn)L2J)feUv%=`)3RU!0OL|ASL7TR|dC(;!0o!y;XUE+Rx!e3-ei7RQD-jEG4 zX_5l<=YCh2+IXNSwh`f(+(&>m$16SAWqhGaWCV^SEq?z*26QPx_SmsQ#Erw4* zNmzs3#IFqorM6~1y|I`zmVTg52QhE434|8|X&jh*KV;rfiMkla{%VgVsoAfu5lH_e5=qlD zXjrOWVJyQy?UIf;L|`0>qVZO){1U8zdWOF**1jPE;kT%&ZgdX zTWN|~)rSFeVzZ)c6YrB)gX>!n16wSfUj2#4uakE+G&Cl!FH5g{`I=+@qQ#`0VR}D7 zFTGrvmz-IKg9LHuB53H5E^6_QuFdht3X-LKNdgjcy;V;!xAymz*EB_?3QoDuQjqdI z*Ra6NpVBD5{S&;LL-2KBVIJiRM z3%^+5s@YHGtX5*y`RG3Ey(s6={RZDb)P2X(FSQmRqIU--&d+a>Zfm zRGhb}SJd2%C>v9#9v&}WJ_d5qX0rOMQN4ZAFKf?ym;07?_nnWHGm}EedleHnq6Bw+ zgG{3uYGZ5E2#H} zV{-aXH}5~Lk-h`*MyC0)GvH_A^~kDBGN|=?7TNaz2Rm5vB;mjWq;aR;JjGypk`rv+ zDN|+|RfDW7qmZcKK@Ud$&5+tn_jQ^%8B3Gq%@vP}==+1^g1(v^1kUuv&HeZ6TZeN^ z_}K!%hqbjctoXXVVc?5tC0*t(jdmLa_IXHF-^8UP(BBJu+fIA*cZsRKE=`9jF@K!C zy(tW35Y+;ul)lqeRc0Of{8+dcT!1S7f`qsy$y@XTJ3nEaypAg^yH>XS2ztRp^XcD9 zaDkrc-d2#OoheCG+!UCi zmda>*=O4{#3Fj?piqQEQ+C^oDtD)J-j2cww{T`{0eca_fttPzqGMc|`&izJt&)w#x zDlF`kYiDb_)Nw4m%{%WepQ@%0lbbZ)h@kk{vy9Bto%VW>JD zY#rBk4QqdxKrEh;5%pi|5R-nbOqhBl;Y0a2?we9I2qnxp-ZeEg(R(nHWl|1JMr#{vcYCl~Pl%lP|f^U#ab z=kNVOz@@WRd>5gu-vYyfi3I*E{UP^3MWEKEprEk9N2r>MAG`t9`&LKq_}nj_fbiuq zn~VA<*Hv;|U0o4zaq)EoRA^Nqhcmr$LPW$)!O5D-#lE_#A*|N4pM`yEUy=8^x6+cN z@krsn9-gUmCVUm&Yv{7sD>X+&)#t~wW`K>gx|Dc2I(I?XP25zgCMx^S(@-LPwGaT- ztUs6Qh{2RCEm&{AO*>=3edIP#p(Uama^}fbT{DHUQavprOH6@e~$jVJR&bMu3O! zi!a(I`uC$74ySpX3j0f2^z z+uVpRbjQ0Xa^v(A{`U`{Lc?DpBI;=XgxzmtudnaGE?n#knnS++{%>fU)stu_C^95l zS)t*TfuZLota`>v&CvzJhx4f!CF2Va!$m4!h||CjJH8zs&ZG+(tC#42FJ{%gPZ=aE z)Mb)A|2l98RT`nSR&R0$xy)g|ZT@=Pp#}}Ry@Um3I&06q z)*ws8l5_1;(VH&s6eOm#*T4)$5;|=pFty>XH+5jGW_0j&n+!YqiXFdAC2TM#ntdY0#&Q(&~Ep=PwQJ^TwcTDAAr@dfzI>8 zYTrNr=9prP4p@gea-V>)WC53o=P~hhrb+&P{;v^E7M$xT)_IkXz{Nq#A5% z2XLSWXxsA@u3*a}fAsOh8naXm=Uw8O#gAq^e`OSlwW=L z)b8+90qfIfV6lXSTfq4A_O~%H2iyDtfVN!i29Nf>$vuT+na*Y?TYOu)Xw)3FK3Ez$ zFUb)N7{cg!Bv)9UP3FoeLiKiS`6yYsq zj2;qBLdiq~b7l%ca45W%@@#+ zp3POQ^eBC|$pm}P0o`bTH)Jw|fr1hiWHHK|e?XNf1(4a)H7ZC5!wKMhG$<=(%~XUR z^&h$hFHuy6vsTJX&#HZWuW3l?ND&*YNGLi>E4B#2x)_)V_a<0KgsqjgyY_j{Jd%Ve zii+ekyDbD*1bdF|jHbZ8Xjfd8A4Z0P1bo9E11T9;xUjFi} zHq`BVDG*!w<>8(^qDlrrC-6*Um_Krdw7AZgb;85PV$&$HXg3$=rs`ZRIHO#0uL#_8 zgNwgL9Q$e%PsOlTlmoHX&3gX$WlK0>`0oRL2SK|h*d+stR{4EMY4fy)ob2#lgTeYR zjQg{26ql>FDRMLcT|+R}T(@g!4-c>E^^uiKC$hmMn>9{Ke~jYG8Y^_lA@W0pu4F!r zKkRZh$0~Qfy$tX$aAXRni=L)K6cJy)hA2*2wku_R1AUj{DWPr@bY6Od1MKACZnZ~B zEsuO-tvx(Eq#{V9zPJ7|VqiVHm$huX=#mu?@oJN%LMzKbcqkW>TdMaJ6mh z_-uMz#m2qlksYqAg2{9@iFtyZok^c7vQ=@mHVM<$R`qlh6vQ#pM8@ z=xjhG;>}jFfHMIhU)|YF)s!TUp-|lIdu0NR(2~a0F$-X~vlX5zt45kNM_M&{ z>=bK1MIJW#_#F=->59MG$+d9H95lQr;ED%GbKCHrs6gq z>fQu!yL6K@KUv^}%$)*9z>3%D)dlxb|IpCQ={)Jk3$g{%+t68d<3ZdDs^RsYW0GNw zZzW=^;BiI==ldput5pJF4rY)Nuu}<@>5@D`aEkY-- z`l34H0|fE-<3E4!Q=E$$Gs!_|`T65FTY|S8XL&-7`wOrtS>VHS+%Je137{6LPur%^ zC=d5;qY5Lyl$EBygzmT5lQJnkqLFTvXX#9q;}(tz1PxzyZl##LCuP82ooQWLI`VAP z8^J`Ph^a&RI#;yRF6&_FXjrc zjuQdDk5=Gkui=|`d|jYW_@&wgAWnux_3I{HGuveGY;9+~lM=2SIE2iQe6i+(tu#6~ zRYEXt4z7fYwQEkoH7$PL`KSOp2CDp zf7o)w*oVOSp^J&rZJg2LvQicHP0wk}mhP89I2J z?z5jN;RuIJFku3ikepbvLY4^JPb7F+qK7j8>wNQoB+r}4nsrp@Rf9!K{u~%<`6$<& zhX-4N1edd?Z>Y6WP1G%Wg^awlm6cT~AcCmBX8?!f4bvAwZu8E8hpUB>Yz|la^IZ?8 z^Uo95fp;<91O<7*lQ6hD=#Li9{_9%i-sHF2){86?11D_^*y{;jq~^g(o|dmpEhdF4 z)nHcm=)`&19B9>K-U(AwF6-N@0v~P1FTnE=Z%;2yT6Jzuzgw;lZ*0W-`1ttd@+OMG z@so$EchmwtB*rT%Geka!7ki_{jrZ1 z^E(hTZ?rp{TXSbiz!Dxto&$u$4bwsYh6 zL?!wnI@`+xJgXdtE}{F)AVywrC8-koN}1b6g6mOp@e#aW*=tfI5wDHo(M(#R7zJcq zqZxZUyO4y(x3|}o{gfjt_w5nvPz`o2$zE@%2x2MBP|>SAadC&_fPP!HT)Q!xjH!^`A`QL6Ln_N#RDPl8@= zlYU!vZTZ~N32{l5n6jxap-*s?$=lgY{P37evh2|whS0K%8*UMd$%%_s$b8S>6L*hK zNT@dNW(IS<;Et={<@iG^{J7u$E4cClq7peQyxFN%pwnh|1mK^3+9Njp#qU^jTl5=L z9WZ)5tz>nEHVrwOi_gln)nz^0PshZ=<2(GNbP$DA=v%7 zk9(2zTvrmO1w*#_OM-!d=7$xN*O7^q*8wQq97KXH3&srSsHoc@Tei1QMLcMxPXPLN zSv(ePI+eM+NPyEs=o?-a?GrF{ED28y-q4HZxqrbm?8ZM;3oP*urk*RdPJ$ zxXL&$HCIMv2nxhLti?lH6cf3SY>-yVLF`|v_6#y9^8kK<=s9*u_F@3S<)vbddswvp z->@%ugoN&^9r-ZV%Lf4iJiyNg6l}INk?wVV9Kpa+c$ptMz{p{0KwT#P-CE(_-@;4X zT%f=m3Y-9P+mO}LXaAI;YgQtK!A8xKuob&M_zw5<5 zHGT%d!4%ca3QdZH2_gX!5jd|TCN{QNe^!P0CQoN` zNg~}_gUmHnynv0Q<<5Ha7j6lGR$$oUVRXK+tp^CS3|y=_c;&Y`d4fB>+CIkJb-x;j zAaPD#Yw+dBN89@PZh|0ewkQ;?qD7}(qg;|TaqzTz@;mpp;4yXnMk=3HLX%z~XwJJ3 z-qvUtNb)KFc3;a{vU?uAeus8^<=CiU1-f~W3G|juNb)pD3=`0&^mm8lS8mR*QtInx z6cFofXb^H+YSy_V%ONF%h7NT0t5=&Q|6I6nTk#sWv7ZAQq38p6pL^?rTY1s83Yl&S;^8tNwtuTBa9`3H0~j;!<$ zY+)ViS*jT5UNg`KmS^32l9&>CGtN?>UaS+qM$X8wUKVgD)ZHzi?cUbG;o5d(Ty${ODs$B=}S{$1X_9&6uFJ7QkJo^RIS?wf7ElMHfp)FjJdA)oI`Bh}E%lYK4 zDP2|{1YdwX>tm8iikM=^;oV#hQn(*K#g`QGFC#7y~DJCFBH%9ZvuI>YR zSW*&^vt>&``!B0M6BC%~j({zd@Nk@b-0C7t8z`i z37|cnL_Lv0tT!E`j{P1!R5DZGB@-=3DCzhZH2MzY=yCR-C|Xy4S$zomRkhLDDsu1( z%OE2&$Nh#_KZyo`bKe6f*~B&pp!eXmwf6}>d7rvZUMzM0ID35F6cFIjxz=D-Ff}zr z-1Utl-nvOgiwrbr&F+ecqmdpk2Ab}_ViR1T90P0vS^j!CsI6GsS?0@G*g!gB=K4w7P#}lPE=}I zb#$)3ZES4)(0lao9`5QN>+7b9u}6I3Q+5BC96h3MRTY|Y&L)m?l~vud@_Zq7ik|0z ziwGy?*Q=PdVuPlRaR53)v7PwjWFH;>O|=rSiMp(6bBCkZ03DbTWx#Z$LgZwy{l`q$ zThcpvsCN<({!t+hx6{!>M70?#{6+-s4(4>AHi<5-!s5B;^w;9L8o~Zc-%BHB(SD~D z(_L2xZ6wb88z$OjW^<{5fj$rOwhtV}l<+$CojeC!4D!BstT^0Gs#wvk6|Jx=l$KH78k@yG`4OPSCXN>qH z3yP?MD9cAKsoi*vl|~HV6#r;{gRPxz(#Eau#P9W5X|m-u1}i|qD$7K**%4MEc(+b0 z=HhVGI5l`nOps3ATE28}9n_b&HnF2>7E-hH@QG(w`OEcfhof~zl|{_QlPY{kU4h1Y zwaa??F5n_Ytul8pt#z|%tH|@&bm8B(VEOO6(s1Y0i+NHYvm zo3|&$-n-SC-<4o{wveau3X@9F?T*HtY%B$4k zp-Yc$ybDh}X!)?!*Uy5x`!!!3vT6W)Z&C~*fl%xva);@pfc!NHWyMrXW^a7P7Lwge z#g^O47U8uG+%JsV0kAsPBNJV>kl!CB%IZl7&@CD^>H!JLz9~y1+etvG@iGh-4=>%~ z)7sQTL`&T13e9JOWWE#=&yawZ54Yk~fgOBN)SiJc8=~jD3U{ZP1EC}r)6$JZjxh-u zchz6&^>K4x_0-cFnvA6?N66JcYDW4gr5*@G?eH_i-rix+i{9@PcDe3yuLC>UJ7AYT zA0*%GYk&p?qrh!65q5>&>>=3O3vfj1>ZYS&6Y;x4lbGH#Lw#=jYhIo@=o#++k41Q?Zre@qx#SxeA4p})0LF*#_24X&gN%WQ=sum zY4dsL!!$L|I*+#`{n zbP!?EV;yO7|fwiiKF^!F`Cx zb9$@6vZ}ZYJ~)f7X9=B4|f-s?G{sv{|=F`wN2Z6R3W;GnA&+q z=GAwR;vT5`dG8h`)IdMO8!l#4HA2wMvTEs@`%=3iFNsee?BZ1n~{+#y%}xiZE)2P6cwofxlh zpVJaKau2FpT>QtUD2hPd!`h);Nsve|Jv!>royVm8c%Wh!1^m-wAe}tL5m#+Z^vCve zjpLRAaj`lBA0${dSjjbgI73@`{(C8jXL}Xc6m0ihPI3(}jTZCvDdHkjXgLmn@Dc(! zc}k20l^y!(v*Dv}TwA#&#_7#~H{|Zv&3%HTVkTjB4%@`ilIMBnBr3VoTE}Cwk5jTm zQz&N4;NM4gIm!Pv;d?orn&+ar4&+`|{m(BN6>f;O^UKZ=kZ*66{dpj3^RJ&jm6kkuy!Y+1 z*Y3SCai-0S_DNp&&NR87(=@Wqkx_}sPrBe({p+*WZ`O;rH8bj_08hev(dX!``|WDC zJKyT6euh7R?Dy}hK5W>OALx;DcUARvy~@h8uO1r{waY!B(iRkyzAQOvQex>+)d z$sfR`Wbm8zdiA)ofMc68rcIp+nju-F=EsN>otTS7p?`lg<$s$K%0t|bS2 z4r`9$Y?cCv1#UX()uuGPATq#Mif2aDS~Jl6#}$>63MpUAxWo&A!je<3hXh2Zfact0 zEn)IjT;?|UU(>8W-Yi$wu;{{#pvjD^B;b|qU01|RC-XJWnm&Dce?Na(QPJ5Zpq3S# zCr_O`eC7Ie_Z@xO?-xIN|6F^D+kPdpRY1!FCns$djlRN|V8pkxOS^g2_U+q!CIWZU z?3oXCnqY_R%b7Q4eEj_N>!+g39YT6f-#9lOyVE|OGL3FoJ$2fY zS)V_9dwKi1C8xN|xR@aT^ta3NX~6SOmzV&(wSBvJ(5!jv}qG?dE6ex zGwiIt+FMy3+HwFLpme_d+KM?bn=Rux12ZOY@$h^pt4yACD%R0gOKVe97uRX8seA$P z@$&WdK0YUAf6C?o1)+^oWTfOF;A+y$iC42Uw44!aP-1FhXH{d~5W)gd9eYh?v+>yI7;WQU__nNTmNXf&JKzCQFw9GJC zI&&q(`IpKY#rCq0lA8!}fijF39`W zHP2_LE(Io4Z$-6jN=qeA0aGCNq1-)1XT=_}W!M`(7Z(v1Pqx#zV6UTc;`Z#v4?$@r zCLkzk*5qun=h}*=QcT#`*mkVUeET1`F!=v}vE=-G;96WZQ1=aZBl`TjuYaVD0(Xfq OFnGH9xvX!=@ diff --git a/apps/site/static/img/shot-load-dark.png b/apps/site/static/img/shot-load-dark.png deleted file mode 100644 index fcbefeac9e6434ffd84620810bff13ffffac8429..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23727 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 diff --git a/apps/site/static/img/shot-load-light.png b/apps/site/static/img/shot-load-light.png deleted file mode 100644 index 7a9263d6db89a56a79bf1c0828877ef15b7bede0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22615 zcmdSBRZv`A*e;la0Kp+xa0_lpa1Rc_39iB2-Gf`uAi+JjyA#~KahG6$hQsdolU!*>mz&FTj6Z zE6gZ7dxrT;T1;5QJ^jxrq7L>R8Nz9DJC^u!d~_TY;wIXY;N=n*sK8NCd3kx>iI&Zi z%e<~zxmRUNeHaR2Lg&^)lRV$E$#ObiW~*ncUS zhgDZQ25wcmNJ|HVg@rk*DJy?ia*{P?uz7`sfDQ)-_xbZ@*&scKd3PO)xR$?n|Fxn z2N`s)jE;7F%%c!CPY7;qj*G+Uj;m`H6BEl9o%E)qp(Y|CNJ&dWiJG?Gc=zsIRYL*l@XdsrqR@S>DCy}n zF|n|eI79k*Zx)?Ym)_Mi)NsC=bzZ>~F$!%hVq^bM*H9fD9W5{W$@89lc4@eKe2%bE z_}_P2v`jaX_L8k)CnDOC&d>FlY%nC|C^mc9GP737~2 zmqU?@5Edh%t;=AM^257LT^x+YA~K@mYb>m|x%c+q1pYt2__SFSpKjTJ181$IMK%%; zq^9mq^)0)5cv4d4{(*~fh;VEaB2avFW1d_%Z@Uo|6C=5#Sd<%*l2RPpT%1x&Pnj4O z7w4*vhlPbzu%|jFrPez@+uOfM^IlIAPpa-67hDbp0+v#>2#VrFSVXk+r%&B=^@+h5 z<@Grf1CihgOjw7I#ofhJRO&vU{&+*At*L2QJe!l)>{ZrzW9Qgk|mmI3MUxA8XopJdGzK)cbWP}J78ChsJf3wCiIGJey^A4xwy)H2w?>19~)PkWdEF!WP zc`_?Tl<5E7S1Pj4rdLqPkdQHt?tg*Uz$7U6T;QWCkM&D1QU^?Y+Exj?Ti7)w8?o}u z1{MakgKIco$>^Gzn};Fj7O}`svWpq{Q&A`paF31(PSY=jhlNRbjmpY7q!pFWmV>TJGDlf^sxD#Rc#c6HJIf&cd}lEs)QWAkl|Q z(+~ZSmz9>sN^KLda5W`Il|}vbEjX^)Kd#t6JTZsoXYixp203MJ3>p%ZG8WPIfTEmg zHnSsQF~+w0mq9G5y~uLtTpaHjKD=w>to!(g=^6U(#|25*t6AfzazAgqEXWxpqgP_Z zRSF}y>TjUAgGjJ}Nk294mGt}G5SCJehA=eXBVNWEGb{6yk-<4BeF^Ok-~(}UeRNxy zG@UBmoZD>dlp2-kt}#wiR|qfD>eP)6QfW!Q&Y zI$xpR$s$FvDXcLPY-Tj9%!DWi*nT+h&u*N3iC51g@v)xf5IZl zilemK_IDqKmpGQUlwX5;REFyO$NI~E*DMY7 z$w)9^OqxjHqov**yZ!az1Yu*Z>lfU4I{rPcS+p0uTMm9^=}MCaHfy;=6k#Mj%Lk74 zW7CAWxDIL(ym;J#c`S)w@@dwZLMrt;#ll}pC1+-4KHObuR~w3Z-={_QFV>nB5=P#w zx~&hbGM1`Ws_7dzJ719b-A}c7I9UYaGOGPqZiI$o(@qQ4^V6I)uKV^pP0y8UdtIL# zpPcA5*>5)3EZeR$DLs(M>e}0%ej{K5L$JTKwY9PF)rOSAa<7@P9~`1 z{gsHzb_25MMWNr}Jw85OZwVn3^7a1m<%`6~dZ*u0X=O)utABDhSHARj8Y+oz3$j$h`MaXhkCiAIU1T_neI#;C)0x(6Us@uD0Sgzhz)xfN1qru(HvgVJ?oKTnBKjAq-COsk zR+l5!!&!QdabMgvizp(laq|aF&r&p0)co*;1&yF@mq+uF7=a5H57+BdKT0NgE0Cs_ z?{ClJMC>bz5Byx^s-y>trKRmyT6{j4+`@Jf8C1JVFv%dv1AEU8#LzoB7#P(n?$0NL z;NvR7vBT*#g@SMMBx918)j3-h1b&>KvmTNVEXZiqdtUzGsU;KizFCHL%4Kp(@Bf@W zG11Y9818I%%GFI%EBCrQ4m?PE_c)cf90MS$fwR+k;$KO zM3>#+*2@hx=DL$~hK+m*nfx&C-T;@QxnkDvq}PdX^fq5S@{#LQ9fj88s3a#vLPJ6f zTO?yhr)AmwpFe+o$k^=r(2|vv#lG6?WR^p+FhwTd$xc(n6@wha{{+@TQRQGKNl+RfDu~y>}sk8Hj*H8oLI(|Lo zY3(WLsj4zWy+RF=x0U8oBGZvP;Cn82p2Ylarq=dYUYECAqgmO9>*H%Ovg^Gi4gt^0 zGUfJEb(G4=f#^#fKf%1HUhJ_SJliAkB#bH*3q=;+D|N-`s2W3aI# zcl7;LjTg+BptR3ceTAWpo+3dN+7Gw5mF4#+%*Nw$SL)Bt3a0XIvVKsB>RP+kbv(7q z&OmN|)nATcia!#94C?AB`|5iV7nt` z6PbN^c&vI%>jfPcQ6CTpDJ*#X9`ETUHYf&S$Rx4Xia*m@2$BlA9I9BP?XG%Ws1*sV zR_M0wSg>D-@G0v5;(Bq&IA5VFI)J=}+`RL(Ch9^$dd8h%nw^(MK1(nvIsE#$fS446 z@`lW==$hrrXyt-W6vG=7`*A5;cA0~`v+quwr=3B~I{Sq6qpiIiYkN46k#y$j+di4jniRdeFVEfoeM~Bc8%xPjx0Kz7zqw{COK(uh;hwu2Fj+odDI0HB6>T0P% zW!^wc!Rl85uWRT11&7O2p-P?h*Vx$Srv@ATfryhMfxUShC~?$be7_X33@P;=?e>huAC1=En z$Cv;oyu|VvNo&BcFG8AVvfaz=&*P`_sc+OY9d{SxXB$F_sS>r zziigpJa!+mnY9~=gz!b;*_BGw%I~3<{^R*rR1(?f6moelC_;(;G*K}Wta3W@4x(|c zJ>Pj-Y#KX^*a6vhfn8jvKxc+t=RbdmZb3+m$S+f&)10q=W?G?D|7qOAQAAt-frIp* zC&afU0+;dAxzo;||12YZ(GJEkXiK1}Q_JO5gnWp}$${y=E=WjWGaF`&l?r_u$&`h7 zewDGj|Fc(YmDhz$>!;3QjVL005CU zLTSl_u+!1@HkHcQGH90h8szFUvCLH>m%CHsa+&;^q>teOU$nKg>tv^;Cepau^E zK>oxHGX#?!dug^zlkIFBpIQMC5fKGa`hB2fOB|pBDIbqEnXxo3$*8vwOQD@|R|kjN zuJ?>#pBz^Uv>R<5$0Q zsAm7~v_ddKsts8vBNaP!I;@)|7ad{5IHdXE zLh2PPDn+sEtMxMT4aZAsO&;gHL)diRUqL!|QtO8|o#9b`?WkM)GmHIA1$?O z%2cZ4ea2_e3tFX>kr}zP;>4j>)<|6}a7xbf@yz1eYd1g3Wy#g-LUz$-3*zt7Tr;q$C%*U2#B}Ee!Q@QXBVho*By3+ne=fB6M+v!N95aZ2b z*uUu{U5V!)Gq_Afpz7fulUl$HgtkEj>9%_KeeX3gCEBD~Kb~#%?OOEmH9u1P;>`Q% zk3q!ib6PB#ngESQSbqhfTWaKoe~i9aE;x3f86J=H@qlwNEa6>N1AXde`xdVEo2i_1X2x4x-7+D@ z+NPW{mo?qwu$|81glrR!DE?{sDrOMtlksWp^lCSTN2yS{r@BMSa#?2SEWTd5=Vbsq z$_FR$H+U7f$FsnBg*~wv=rtjT_awV{>N;`p`s4JUJbw3e8tY zyRik3cJi^0roLx`9+9r?824p-{puI};B@szbDdHBWVMygu~#Zi+`Tkd{+8(lqUZYG zYbdLa;Uv3Sxfbrc71(rrwMzgU@q*)OTTrkbJ`o9 zM)}q82r%R1N5gBamQnz;DH)Z~zD^I4HhB7pwXia*PQ@+xmafaUOT?dWmQEz#lfF0X z1zobhqU{g>sN~PT_R3|FS?J1(D=8$Sxz-b3;aRYSat7eUsSzMi1rIJIhxnYVOp8tW zU0=4k9j{$nK0l9(QxcU##ZGa8%?`n%;OksnChjdPENnf=v|ESce!}KCR)!ug?A|Cm zSs2f@4%mi1xY1d)D3jntvWA|lwnRM{+$Y&+RMV>(XX+j(9{wPpFiP zCZ;cl`jp7<>L{c|QghS=CAjQDd@$&9`m~9{6FP-Po1^#ufFL^r^NjRAV-ypvU^PK*bb!tuuUH5y^eTYg#>>8d$q17M~3BPN1&vQQM6%>;| z2npA`Ur2IxV&mYDkqW!MdTdmW;lh_-QO5bp zLQnTjD2ZSS6?m15jm9FVps2DjnW1>3;j`*_tLy?!p}Z73sQzxf$sungYZ?Gx=ccwJ zq+m}o+Lo+YXuPDYZ?+|*L>k*&2@!)-rIV4Hn|rm^-Z!Z6j(PHaggO*J;{x6{7>n}0 zxPUSy94f=!*NM2$s9o0icUzRarBy-F$dvcmI4NNl-3_l1G74mZy5`+)9B86oOU972 z`u4D{n#`Hlzths1uENNV8Y6nTbN$memd3%VoL}YdFFbfu{rUCYJkQR zDRrK{lAT}2b-0W~dz;W*Ao?ASNQ(Ugb zE1BWX*P=k)hIK$Eb3~sW|JvQMBq-1*WHJ>z|I=1d=+8vJ`A2RSlZbwPQ|SvCyLl{s zLX8`A?Jx#ll<&^jG{YgVO^g(N!2pA)_>6m-6n$Z6L|kGL?Tbdu z*qBuo=lX{-h7-cPde<8{fyLu*F3wqnlI>Z;vp`kbYPViKJ0u^z<`&s|SUXgZYHU@> zKN>W1hW?aGtNN>*BLdIfqb&2)XX|sJyg}pZ-J_X81uCa66MBM?)Q8AUzekIe@(@0~ zD3D3+a6Kk8&5aYy3MeQjP~80r4v^ak$NYRrDu)mf7P5C#*~y|>f@6uXr=OLJ9tY3Y zwB@5(u65Z8?MQoX6rxywX#F~snxECNab+;)OLnGA?-90Rkv5f_TYDNN7En9)n!p=yJ? z-eEq#DcbKly}0N-|3sY&@FFp!fy6kM{U1I$fpxWb%(y6cd}Q!7)ko^_PIa?Fw@i>E`gN=87SCUtv0&N)Bc4aUn*< z+g5*g=S5Ld1~6c|*Cjx1xLOy!aCfaZE*3yJNtrgWTki;_xqW;gr40BgDd@70aKprc`7#|$0KT^*U@>Bh- zLdMRJ0H+?wUNryF7);ol&li5Eh^}!{i7fi9N8cjYZS3q~l$+ZcqmnSlJ|9#lpta>Nhxwzv3BBiCTN8sYNE0>1#ZJrFZ85ypvd4LZA+_g84=6Y1C*7|%1T0a`v) z`bvP90sWTu8jbW8LH7t`^Mh;`e=f|e6)myZAlS{_0$M`B=Tu*EZ~Zd;&JF+or-j~R zim-6!M3KGw^6c)zjNl=-bt1Tga}~Oba{6uVN|`a^9B-f9YBA997tL!`cLq-k-JEUj z@<@^D``%KQu>O!>QF_u&VAUsO6kbTAr23?&YHboY1}p) zPc80{pWmveH66V-UEvnXQZZ5=Nnl|!I&_XhTKDB*S-qW`<7E3emf&i|hGAC|UQxf1 z)mJ?B_u?Q>4LRz)YqS!UFk^7W?}M4bZ5gt$u0M;lyCk1);i7`MtQQnZ)tE0&a9zw_ zOULkV{V4DktO-lo zxMIlF{+!RS!sne$&zt^_~Q;@7PXR4A2TzWv>6M zZErln?bi6FXC&q{d)FB08en4KGO}-){BZ1wbn=xH&ceBBE>Obni^NY>V3ZfTm=dEm z%%bW1`%dW&Q@+2s40Vld!9?#%Y)yCS3`^)_(8Q5!o+l~?(Lu?3*apI}E@@KOK64ow zvmT8k&8y6}>e{2IvVtN@liUdC0uEcfNVf}!A2IrtClbWP@&&H?c{1Oc+%icfNMA{$ z((`y5XZ5{_o{$5Aoa^yY!SPHAHkL{6bl2z#`DY`3`cfv;Q6}f}c9URHg`mglbWUJi zCFLoNhe}iMx*=U!k1M|6ePiPB&64vMG9nh1MJ8}0zGEGg5hl&kC68x(QR(jjV!4)q zfq9Z7mn4y~g2%T%qi8Noc@h8ohq}JG$>F(oR)uxB&Wi+EM|XTf|9w2@!Eu$uHCB$B zaJP=D2A4NFP$E1`s?{2mSuV)I9u{dFO)*AEWw39YAXDh2Z2X@j?qdES1C2zVMlysx z_oazEx@f76L_>(kfV@E_76LjY<=vY}`&@nU&^XBXP8eOVX+=p+B^b0*JpvS60di{H ze+RiZ%f@nHA5NK>But^=rDmZBHv8z;GuyQ=-L=v`**)7Owomfku@)w?e}DKeL~Z?D z%(3o8HPw51YJwSHV9-;Fdy}FD`G@;sz5Y?{kF|!$DC#7@Xps*p2pBok)CJ-YwE#i5 z{~Uw=7YFG7$sPLt`8|1BxPiw&$(BpfZ&{;4JIVU^Uv8T_S>yNK(lX{JrVDN}1INto z@?>>+c=&LCDi5jyn=X(6GC?UQ)E(ZpIyyRSZEdFG>9-rd;DpIWHGmm;P??aB0JP?I zr|SIte9$_|HL682u}MiuiHW_ztTm2413iO;Pi+$M3vjk5+k+=hff9Cmd)w|bRM~L{ zP^apTB8_T8pzExxt{NE{+OD-VTg?3Y`t>Ui?lrZwF-X&cX%m%u{znTK<}LW|Hryes zQUYc?au4IOU5TBR03v4+tA2&={EzoWhx3)tqa!}6xiUeYyB{ejNS>@8j#AUHm;e#lgB>)F!U}}i&RFcuMF3HW! zx7oship#H%kc12Jy>DRwYmpuB(P3eOx?UJM9gfWG7s2IGF8(iWS7Z`TEmuIFovZbN z{$iZJ7ZDMOlKYlHOk$~4xs;t*2)ti%pXAtBZiB5}kIpBB{a~P6k}h=O3oSR=q`bFs z^?v~m@6)dg+N(%BY~bxw|8V!@1GcRr*)Xc#VlwEWWZK9KRf$11F6vU z8jPi=sHm`*4?)iW-G*xSB|JREbyIUQhxueSI4{`MK3+RAaWad(Zj-UNwNCoQ5n#t> z+g`hOiKX+kI5^{;S^5o*Hs0QC7gpXPB3-M0Z+P5q_SJh3@R(v1V)cmLZtDYW0~ISjJP;AX^=OV6((ZNj=T~`qyFjXU+`o(l zxCNQ)7V-SF+}tF)@B{u$`grk$BAAx&R^Kv`F~980e9V@k{IE=F#-eUdq<9NzM&s9vy?eTxqo* zx?Z|WOTi#6ie}O%`_t6}hOFpweyo#-VeV7;@_>9glJA}M-9+bj3eV#7v~p}_%ZZyP z>rBW_+eC9Gr_D``k(qFi>w9`07Qv~NZ$6COtE(iquU-ZK+^N&*S`M78AJ`-$J`Z;; zbtgbQ>;FKfYwY-Jpj^9AaSiYQ)VaXvzT5M?zgNeRqwj&G1C;L74j-?jItw6=$;-!AR`7gNBYT|MF&7@hA57@-CE(x!5*Q5RQ$tn=8NqA>l zT&ye}qe=KM-K=eF0J@JFj%fRx#H^j$;IE9RMp>z^zCV;Ap&t^w+T?+P%~7fzHxtr3 zI$P(noRp$7vCF8g--g;F_O!OTTSwu*d@@}mtSftedEXV31znh`}tw?`@?YMn+IDwOkD>5?DZ_l$rN7WZU zuT5#GU2Ms#RIS4P^i(My?Md5hIEjTC6j#p_Dg2*^5aSrj$_a>4q@_=-RH&RbNpMm{aGrRI;6hzZiW4|8Rj zf#E~nX$zN&z}2^Tx$RE2gW?n!(>*kF0IC46EkWhgXmw>4Q7HJ77=h0keK-Oza+ZJ> zjdN_K3U^tV-fno<;Gi>`q^#`E_@MfF*sE%hari>HN4-h2p~dE|uu6%?$IkU2LCe`D z>F+=F0;I%U5Z+}zDtV+TS!^*6q;9bU#ZOL70^=Gm7L;Sr%3(kVNcr8X?A8TU^u4{k zPiL1^)hcujj*o4=|3Y_dD4@lSCN9C^67h)6o2V85g~&QiKE`}(OPF_;|k3?2OJ`n|biemHRYB4dgab3_6W(6kzD zS3!ZezrP2(+SJrE1SepW!Z|AQB|6u9$=imJH>y=v>P%0_0KP4$<9-)y~&?-Eo zU9EjKYukIYn}VqxeONpf;T`VozCW1RK*4AAxqiU#e7E)M*RN{n8B-_UhgInBj5ZIC zCEnMjKl?f7@S2?#505M)YKIpWHB-5r_Z1~Zh`H@Qrha^Zsr4HQwJV^^coGdMB;|25 zF4wAm=L7uYe=Fig(z2jBQsWlagB{u9p4e{0E+pn#R78Yc=NVMCqVQ7z?CAK9UP!Rb zy_NQvq4+s(JI{cPhI?;FXG2J+B#sk3<@H7^DdXnvyPg~9DOH$MCC~{tfHg}&K>-R! z`OkPufAL7X1?l$lERD3;W;ST9=jg^z<}BmqP0dEG+|QUxRd7R8BoQvM1#G zB+@o763?#}Vb?f12FSD3O_`4|vfL45Uae+f74&cK-x7(om<=adGHq-nTw}mQ$X_pP>NWCZp7O2wi z`K-ch9EZymee-=3A|k6~%3va{GVPk6qlOHx85SXL`%V|4r;Z&jX2^RcRYvt*QFLNn z&jVOu?BrgAu{fx5x{ z+3lIV$RicuHywS0!x`%zO(v8H@`bCW&UsC(t$7>%$5_0B^LhdToIy&?6$L*=?vA@6lH5ksz9)Ogn z5OX$Vky8%$KZ5KB5cclw?hp_VmKGLvMv^N59?z4As;R4!b-M-iNg+${;o@KhgOvXr z2M4o8)fa$3B%+BeY;9Tgd-jbtq|~MMo$htoyy0h}qdo2}_P6v(fZOog7fe6!gY%1~qJ^kb=$hDW-KpOSwr=SOz7s%_HL_xUmpm zkgm468Rdem31&=*Mm6Ph4+t%B+O2s%J^IWtkEio&Z)}jWLXVF@oMUfrP-^sFHU!i* z5HB3h6o}k~uC*_>xEz7rwl|ihKKXb07+KQd+S&5WW8(>vRtp3B#ck&vO9IouTy{kv zc@*JnXY#+1RP2eJM)=SRpoOfgZ2ql)*OfKsZIycMnfx9##zXNSg{72QsYn-sK^9%N zxwBK}2G!%@;<6A-vR?1-0emJ_iAMIv*EIrRsEl8Wim`1)pQ-$?X7off(QTiB|Sg0VFWym`K_(3+1I(D{%|0o!rNeV z{DYflA*AIZ1HY&|FKu`#RD-N4rNWIuMG*0jE*-inZbKpN#OHT5Og z2j2JZt9^AR1O;02J#Wf*&MWlJdwRQ{9;~gb@_f}Nw>Rtt6#B(tD)c#RH72ePFBhHm z_67}FHySm;GX0BA+U#&X7uY4)?6{Q3aImyeTKzpQaImg)-j0~p5fuXi1!aIY17*i< zHruwj=D_dn`*&PK^arQk`ebeW0Cs|6iO;gLQsry27XUx4OWLl)=LYR0vBzK>P{SR} zYLuvFz#UGOYu(8&7FtJYQXi<5coJOwJoX3D{<-JQWn90_`)-uyo-hkDX0Kco4r+tu z>|da(Dz$%Z!*`WUcX{6@^H+yQCduArx`>qY!|!Dzg{Z-A*=r$ERVc^=_&)!96}u!fZW+m z1K@9Jsv3wuq9LdbE-rGd4FE%e0L$vc1S=igD>>YFMF1=m^Oc}T(-e0E+}WRYky*r1 z5Len`qH!;~{8j#5H|UyczNZ+Imi-p-?VHF^liSlKP^g_*evq+6MMX{NI~~9vR`YiG zT5QT;>u7!!OMBe~2$zpDl*8pJ1<0hZ(Vcu_v2;K&sGdnXO{Q+`&89f%#}Gzjpq;VY zd}X;eO7z4A+PBO-V9w>T2g;&|m(>k5%KCzoA7F(RoiBxaXeOGypwE3Dd0cBuqe=p2 zF2b!>tIdwCR$-Mfl$D%~|Dy#2kZvQ|>4v(Ni>Ymp!6sbN&`AYpCWL=<#Y_Z8u74xV zwA{=aP$<``eb2+Ad8g{BAfL`#nrS2${tz0yetL~B=83jWdvN1EdmjkUBDiAHs~jS5|H$XX)R$_Ya&$Vu zP0EgfH0pG|G%S5%V*}I+kdwgB2`D!wZh-h^H>bkKzNQFQU5Gszo!FCq}ycunD3fGqD-$AYvRX=q)v$S#reu@nAo&h zBJ*d4Mjw`hm-n_Su&d^;S|g*2oeC-3@_I;|c5|%f`E~y2KO=TWczAgDeP4S&cAJ## zTo6EEyTdA_%1U}qn||_A7CnG5GrY^Dd~JW7**MWRrtR9{v#fm9ei+I=SL5-W5W=uG zZsT{P))i+&%c9rfxLdNW2|b6Ds>LUZRcUlslF%sr5ze3iT!vMz1GVXTW2G6zuVVGB zTa&N%uL+S9&V4rj0ILGz-E?9{ln2L?6`qw`wW&&dA@&`Zw-Bxh38(eKzX&miuUI^S zZZMT@lnVk}1m(3Liu8&Y_&t~@_CSG1-~uAb4Dd%2SjMx2)*J2gjiW)Y5cIs*2j((( z;~WNabado(KivQVI0+%)5{R0DfZ-tGks+EpcgJX4tX7@*=H{k9rW5EI@F;lv9~gZluUgT1SUJg6 zB7NS*el7OXU^npYx)$N3?GjwW4zul7q#CXBt6RFLhRUl=*e}y(*sSn1V z3>mkGOKL>O=T0RRuu@5hqdFxHx&?yBh=`ho2H&G|6n*`5kmVf$d3hfo8-%QU4V?fn zEx=v(^WshpCv)Jma)B`NpZQk=Z0zZK5)#qI7Hg<2iRfI4V%#CP zciumzxcs+wONIS_;?rH8lVfmQp+5{FFLhEmptXJ3iibj~s_b*_-8Jz6Wl0c^e7Y#n zX|E_VsIy$dvHI8=)d@12s}fq6%ad$|Ab{k`{5zm4VH}2bxP{k;VN7m;Uc#+>^eBGx+i@wDoA-Bd za-WemF$rwYLNtI~O$pK(r1Ey+ets|DLgMmpjb7CZ^AU5boiJQs?=dhmV81rUkIel7 zJLk(+r2Ht5{m2sZA+%|8wjT~c!UoKQ=29oS+h2oR;N@{6``LKcbHKAGu@!Zco^Tbw z99ELu8BM7L$&y329Arv}mf~%!S#Gz#wu_p1OX_@<-KYjlNvAG(cnK+1K-ciKPUWs+PY^q-H6ODvgaG z<8fTTW7$c!-1W#%&Z_V3UeU4KprWJ{@1LZ8o5*+wsYtPfO0@G1w_{u5@@nSu*l2U4 zy?Z>0>p}dfrI({P$VMvUK`AG-tt2Za$8V)3(+aaxWsE2rJlgr;YTD5@oB|*SY}tKp zTsOxvZ<_hI=(-IP%Orf3hf=si3IhEq82K;tk*Wr@i6buyLmfo>q^un8j&EkRS7-Jo z7CnMqy+WrXL*Wk`K7-0DY*zh_1T#FkY~t0J(YLBrvvTR&SSB$*o=Dfz^LE09nw8Nko_qijegban8cVzT=rW?uNhp3oF@|wMZMEh3U#kvv@{#|PHwQ$;pFy|$EY3gLr zS?P&*Teoj@ZTDzMpeX{^k^Dt$VIl2L^-4Wv61TjW3lCIDsj!-okcjraBc}~X$$>D` zCEs^!9DVWo`eD)Galg5DhN1)m3MWJqTj(@JZhs~A2PH9oMsC~{7`DE#VjRYJ|Ngzy z*zlHQRn3i(((w(BrkaG_V`$yjmt^5`*22|N-`M@4Hy*!7s;X42I1Ovp3Hp`XyKK;x zFV?Z&I^YOlHjDi>wg@Wr&Q@edS@fUGj36~(J^QM0XRUHU$9NH$P6`y#4_ZG}ij{%L z=W)6rToT$wZ7;RTeiI>@6*ehIOIrxqH3pgB{l>3AE7f;wpd5je{ja0xDJrAOhHn4E zhk}t|Q_=Q!S{W0n7k?}`m zakrb=WKq;B@mcP2ch%Gkhg-O8 zRCDb&&^141&LhqrN4Z^wjE%`L^y;;_zt0Pm8YomLQv9{i?nYMos+|E4 zob&VZG&VDN07qbZ6Hfrs0PRyDmAx2v_`swPncuhB;P!>psWc`Jm`2RQKTS?vEb|l# zQY0W+b|6(LKfM}$Q^dY<+8H%B-`gt|je|SP2)%4CA#OPkT6*w#d# zN>u#nQNMFUkyScxD>emxYqWJ!{glS$^QXvQmAA1{f4(+I+rjv!3P!TwCkg0~Kz&>$ z;f~gdc#%Bbmfw;5l9xg{FQZ`b5=W^|_qJoWAbQBf`d**UEYaMq# z0r`r}zPGQ>ZoOWiX#8xlPRYlkBdxS1RlRGhkq4WeFT#M>^Df zd@7M3KR)v)yC*O*tB7(bD#jt$=Pzf$C@^2;D-rH_vlO&;b?zlRskj$hRZiSfkX&tP z)@lLe>oYTxs?V&n4QLV5!7?2d>b}Z+Hhz~SiL$Z@s~(JMmB|VrG{R^Hr$PgLv?i98 z{Ep_Gq`IADD%EW(M&kKgC2AExG1pc8?NQ$@YLSZc^>6C|b!Cp3RtzO~+>NMVVvgF0 z^}gLrZbCyw1|f5erBLg&HqBJ?`HGB5K|en~M%-Hu>Q`jI+A33(@yPB135}qNsh;=5 zjVCXeH0thy4`ZWTZL~{b5N&6(*QT}$l?pWKO}9@e9%f^{wF6rVC=Nk~gE*I*2oN^s zY+xK7dqJR^d6GXq%X54c4e&%z3!pjg+Vrc^V$xX?J%`-TpdCFm+hUh6wO)~$FP4Sh z>4ruVf8P?Hp|O+4r$ZZw#qdsq7erm^u(B&jNn}jaZ}nxni=VT(X_eX(Ih5?ry(cG$ zD|JZ1!#iH81KXjrC%0+WA5XDG3?98b@{9~$oUHLVUgS5YXo?sHcuS2GZv7Y_6}sdv zMWURh4})hj8yAZ?1@Y&9i7l#8RI!P+8ONpW`Vg)D}B7b#sb*sm~&JPqkCiA(nl*FZGxYOJhaEZZySi0ENRoWt= z-xABgb`Qwl;fL5pnv%-=ZZ6_ScJAtEyH@J^g}hR#&b_jQ{~DC{B+Yz-}rz$BbnDaCr-zxEK?(?eBd z0P}v?Bn;=ttkHJ=9TA;!S}6<#a7wV-fwYAJ5SX=kD|H101@FdjtvQ zSJm5FeUXUh#IbjQz-0y#agq(^G2|KS7*iWSsrzIY}<%{v?w3kTyMR}8UdPazBZTo@`hw0%$=qh>Y zoTn-WKi-`Buz4=DiH}t3owvH*MmU>S$hIU!+;RV`$XI&dcN2URowVMT%(SzzVgrh$ zkXo5Ax2S%|I@*&#U}E7<3yn_B$I6D|mB_u-2BVlZ{^9ZmPq)8&3qyQL*FirG0{I&3 zW||CicRE4E3lj9R1#m)dx?G%X7ycLr*@UZP_EaX-3xw}~X5i!fZQsB&ASA#Xl>71S`6bwn_kUGjAH8$5 zwX}XWOr$KYfiT(MpHWAAJJL^OxaNDj-cu!NQ6=VJQv-Tz1zsfHc&#Z%f~ayvjJTnx z>2JN^<6+fG+#cDiOg+rf8^R>?m0{@;YIlVjGq`It@ad!doTXdJh z#_4-8>I2Jac$6Khm2ADG(!Q2vyfgu$>63QEiodI36urH@`BDaE7mu^0M;4}*yv84) z9w^DfX8MWQ&|;uEEW4ecG`W#6x;YQ()&vo~*4CqyRB;RXDJ~(AQxX(z$Rh6PeEj2Z z4d}wtan%4XAyMX*^;}=oSU_RX{l*;dyuj{$pv?ivIQn!{^i*~ zg=LViSc0j13A5);Wv&AC zJ1LLTpWanFh=xO{g3nut8}#G||M1!GjCKlEJ2;>ndJ+<)fnD=}BmZ%Fd~F+im~bph zBqrt==pQH;#X_=7KtOoB=5{*0TWWzZXQR>SIxkPQeaZ$RFWd_SB3aWJt=A1U>rR(< zXu%=|HYU3pv8^gdjanR~c!OCA|8^M{EVAJKi?vXG3N?ib71U=2+gZbEkpBDEH1Ge0 zFBz!BMjaWG<<}bk)3uopte4JLqxxpP6vBlv@@P_&0~x_CPT5NyiVEJ0R2K=5ld2%6lXUO?A{Ky)q7D=;A7 zr3E%z=s0+&fivJ(;b7kYc-q3J3E=I4S#y&FpWihF@*S0(kG!k!#KgovXhuNKFf*_f zG`OD!PkZ>V0m38>TVk>xO42tlP}n*O0t@!qV5=)Id{CxeAfV@7fagV=_%Uu=WwZi? z0!Qr0#K0hqR>2nZhsb6eEZ)AZk3Ul1q@||*NJ|Zy5OsET2CnM%)>e9YI>>8$a68d~ zYeRWTOh~u@Mv^QJo#Mw>S8qLkqiy|VS16yxue?9|l?Yycb+|p2{{9LKjZ;zr3j&Y+ z&k9gafVIii*-*5<4;LD(*R(_Q`gLl2d_1k&3G(iUCx^us8+T6#!QA+wK5{sUmQjkLW)+io4w8FsLocv-HZ1}KU?}!RF8A$>$7wMvoBW315SP1($9NfTc z@n^HqAO0G(z{vs|+rWn9Ghk5s0Ncn8HhV$=$-$ff48sq^UqJB$_AN09iIO702jj5P zQmduCsIM=?ulQbl7Fuc^a>neIxJgv!$0? zpH2aQ(ur&>VkN%6SWJIn7As|)Xg=^G5?bg6`d4HbAWEA2Myen~GhGQP#vZFqzrp~z zjTWa>Zilg$*iPj+o>8b?=C<$ zOZ`v2#?uDab-*p_>Q&eGQLWSyptsMz+@GSd$o(R`YilMV(*5xF8km|H85wa%Vt58I z4p)B`LE<-l?e4{7(0Wqex zzrXz6DKIZk)Z+TJoqi9*%D3~e;|;c&T9O5F zIs`E_1x~v)D5&1fpn|eL{Y59n(yTUFJ~5-n@$qz}Kh}Fa;R{s93IPBtJnygPbc^(M z8YZZjLyl8EI7Q<)DALfYcH^=hgYMR$UXl0qJA@Wu@t^caCzC0F((FT z`0Xw1v9XJW|JBNQ$5Z*YalGsuaqJzL$2_*|ag6L^MJ36KhM9~cD~?fCQuYpI&xlAy zoMV+pHYqDYisw4N=a1j(`TzO%953g7-QW9rf3NFvy+5BDq|!v)#KwVvfh7vZs}dsT zbCQOuUHU=r?>mf+q(XFZrWo8_8W53LhJHl&1{~!u%;@jQZVCFK;lo1nGodelC^tsv=@|2A*6G(<{ zNVwHz537EBUvSp$o2Xh=^6PQO?9w6p)B!4&I(DL7c!Nyh7a9v}Q%=gv2%qd@=LaTR zhLbpc$ZtPj$ew>({9#UH0hxl=o4GjsZR`#rOANEQvZ-5sCoYXoDt4%+ZcTyIoO!zC zSc8R_{H+jlrX2x(ps$*1y{xW=V?DLBboByr=%0OQZ`>Pfbj~a=JZ(szXlCj>Ssn@k zlgsi1-r;fgw&xSo4cp;*iybN~LR}EmiQqZF8yK*lr4@S%}3{d}jOgGn| zc!m&MTwb1@E$~jcd-v|0!5Fm?ubk2F)ZVoTME2?vJzP>hsBs~GnFmvJ&EJwX5%W8y z2Uq1O=#&tJ+fSNR*S&I|sV_A1Gu?Lx#57VsD zfaa-W$OqcLJ{7iUM(>imo*=H5ti4hS`sZ_{{3-3HfW4_i+_#!V8yaf4J3+}^VtP3t zJ9`HF29pgpB&}W%cb|U1nydsn+VU7NEeK^MqVFfmLW;GpHiG005hGrMky8F&1d3Ij z*fS1`MKmuFUW^Rq0|a!YC+o(&_b-$L89O{={;)D@Q+bi4%(>j7jXSrr@^<0&N_3I+Xz8QPGyIo4 z9PDQJs$NsHt`_DP-6Dl2Yj$4v1H{GOgZ+Kpo0p0FNSTmdnp%U$t3bE78}ypJF6!pJ zqZBYf%`&G_zDS=3hzc>auUUKY8Im#a) zyD`-iL0%>)$u(0Rr}nNRT!VM-ajC3=g0>BH<}5UCt&6jg7_XX;$}D%{aI?ZEs!cxn z`s9X~B&#GL@BNuhHQ%pY`lpC^3dNv>krJ#=c4(R&i@$kl93}tA`!Ilk2*~ zSK<-RDoylE61WHByBokXg7+$2_>hzyu7>XQePpbZ2J1i^Z4!U02vxiMf#!L z%~|aN9)aiVavspZ7Q(n$D}D9aS2MXCcRJ7CRTFzfSkfD-yuD7IaUBd?KkF?JWh{My zJ|C~u-h1|~G9!a^_X!;EAK};8(A(X(W9q^$L=cmOr}ZNL_LX+Idwd0ryObeD_5B~f*Bhfe$BlE%U@)FtZf{u3*lm)6Y6m{S{ZI`rNLvF$Far;D5N0?cg7yp8hoHU1 z89WHh{y|8>*!lI#q#@zav(*KCWbrc@?bbE~P0$NfSI!`V0erl%-j9=x9QF03$5xb- zvG|u{hZE#+MPkw4oQ1g1k1FZM9B;?Y9Ytt}c#Uqoz?I(>Vtjl|Ly?HZ8nxUGOndWA zc{6uOQu4t%Uu)rt{^qZy;YO!vtG&GR<=@z$H7Oj3WsW~v4(!JKR@&H}vf5PG8?5lA zsC&H-fAFOVFhtZVeZgrLXcwT{mXZe04p3$Ul6U-`X^yDm*=?ckOsjG@Y?5pHxXhtcdkNqH>ryMt zfWsw&aCADtl@a!7-of zJ(gu`B-*cFf0yvndW_|*z8^Wx<8fWg=74Tqcy`Gnz3bQek>Ym=*`a>~SliI}!vnip z1I~%r%5`N_{CKV!j7&4UGn&+o_VXbC;PT}hRYny@@B>|djR#XR0%u4_MTyR-4=2*L zwPj`I0M>C>&Bn%tg(V$Od>CZrIQJS>L~x(KSG$O3Gxv)#A)`Zm)+Yb;M8=uUqO*~w zXtZk0jyLRL0XgbRhR;f`bSdJ-M7;}+KN7Dt)!R$qD5`Gq{8MwG4M=v*^H&ySv2$Oz z9aZ{__>Z9c7ZMaCEvNPF=px_wq2~Fawb)eq_21dzT37aNnZI`f(rY4XewNGrl(enB za`X72g$8BtK1C_%BE9c#&~5egbn_=a>tDs3I6*#4$PRS6S|F}icm2!G0^1GWPrL}y zskROC`q~KXbtYXZk5jhXO1$E#T5g$VJHU$nK$C%Nb*3%v(g^hrXbAiC=>}ry&7aF2G@h=_h_8L^{z1Eg0yB)17KJ5$a zi-v9lFliXYTUP_+GsS>tT?OSIXt=a^zoBL$r>mOnI zbot)WSKPv~f2IQm1jMT=L$bMWInbpJ?`qCY2C8mG9sjT_KE|{M4ip>4S|IwDnIzd~S7G7Up7Y;;0AjS%BM2zrY7B@zj zE6wNVdPtP;>h=z3m`ek{4U#e!%g4FSg;M?1w-gGo;SE#UZ5ck4z18;uv^4pOOamcr zA;9P~FDGrg4QH`sv51!23J4X-BimX-BtUO7d8WiNLb|iw4k?;o=5_k8?*BqNHYUT_% zug=Oga=1Z%{~S=CIir2(&@|j;QJSYe_ze>uzq5It8oXAx;ET?R@UMj*qg0}_g@#au zz)vXk$YFdvywGKtq|wAm|=nXH}%3p#ce6nt_{va2L_8Z8V8*D@GmgUA_9?rhu0OA?1)BH4|&_zDG}QX=zC-A5s?KmtRyMX=!QAu#|VW zdl(ZT$#RvX_M=-PE;yf|p}rbF#nygNIws~JBC`l>t)r_iB*t{Mr{~W1AMsc$Avw{- z29y>wN34NCR8$n2Lr*WWeKgaWh+Nph0%YecA}KE~7bjOw>;x}i6;(9-cvbmDMeW(h z%y^->k{J!WrLB7*T>JDtk)G*!X8tcg1+&kX?;&Y2P)3TtFN9o$1)^Yk|Ni0rg9jfQ zNoq&63kxPH(k?I{;)E{eGz&Zp|2NUoxmY{|3ZTTjGQw3}nAI?WHT^%Sn$3v;NW{ff zEF_6OzU#9ke@5Gq@mzBthoq?~^cx{L(kABXzl4aXPV)TN-kGH3grNnRg9IoqfRn9JBx`KAybuWPbg3eK`7`v zZq?b#<;^X^_sUcVI@F>_*@zqEAkOw{M_Ttgp&asFiD)cgM?Q0br_Mq@SgW3f6EC75 zjvS!<^c1ot3;JnjX>mE!V=e{{qWjM>+=2o04dCpbN~PV diff --git a/apps/site/static/img/shot-plan-dark.png b/apps/site/static/img/shot-plan-dark.png deleted file mode 100644 index d8336b8dbdf047eb8d2d5e973afe9181bf94fbfb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26709 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 ePX>Ist$k7 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 b`DKEP+^tcDN|s;X9RdXz73pHBSN{JCa5Y^B diff --git a/apps/site/static/img/shot-plan-light.png b/apps/site/static/img/shot-plan-light.png deleted file mode 100644 index 1518bb51104e404f5c5c340997f5686fd1a59855..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24343 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 diff --git a/apps/site/static/img/shot-popup-dark.png b/apps/site/static/img/shot-popup-dark.png deleted file mode 100644 index cbb6624c41fe74a8aabb869cd9204e2111c37952..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112393 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%0AfQC( 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
      DU;&QTdvlI; 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 - +
      -
      -
      -

      Your internet,
      honestly measured.

      -

      MeterX is a free, open-source browser extension that tests download, upload, ping, jitter and bufferbloat from your toolbar. No account, nothing tracked.

      -
      - Get MeterX, free - -
      -

      The quick test takes about 5 seconds and uses up to 25 MB.

      -
      - - - The MeterX popup showing a 280 Mbps download, upload, ping, jitter, latency under load and what the connection is good enough for - -
      -
      -

      What can Mbps do?

      -
      -
        -
        -

        Counts use the extension's own rules: 25 Mbps for each 4K stream and 5 Mbps for HD. Calls and gaming depend on upload and ping too, which MeterX measures.

        -
        - - - +
        +
        +
        +
        +

        Your internet,
        honestly measured.

        +

        MeterX is a free, open-source browser extension that tests download, upload, ping, jitter and bufferbloat from your toolbar. No account, nothing tracked.

        +
          +
        1. Connect
        2. +
        3. Ping and jitter
        4. +
        5. Download
        6. +
        7. Upload
        8. +
        9. Result
        10. +
        +
        +
        + +
        The real MeterX popup, running a sample test.
        +
        -
        -
        -

        More than one number

        -
        -

        Speed under load, not just at rest

        A line can look fast and still stutter on a call while something downloads. MeterX pings during the download and upload, shows how far the delay climbs, and grades it from A to F.

        - MeterX popup showing ping at idle, while downloading and while uploading, with a load grade -
        -
        -

        Every test, kept on your device

        The side panel keeps up to 200 results with a trend chart, so a slow evening stands out from a slow line. Run tests on a timer and get a notice when quality drops.

        - MeterX side panel with averages for the last 7 days and a speed chart -
        -
        -

        Against the plan you pay for

        Enter the speeds your provider advertises and see what share of them actually arrives, for download and upload, over any range.

        - MeterX comparing average speeds with a 500 Mbps plan -
        -
        -

        A report you can hand to your provider

        Export a PDF with averages, ranges, a trend chart and every result, or CSV and JSON with units in every column. Each report carries an ID that changes if any figure is edited.

        -
        First page of a MeterX connection report
        -
        -
        -

        Private, open and yours

        No account and no analytics. Results stay in your browser. The test server keeps no logs, the code is open under AGPL-3.0, and you can point MeterX at your own server instead.

        - MeterX settings with a choice between the Cloudflare edge and a custom server -
        -
        -
        - -
        -

        Twenty seconds, start to finish

        -
        -
        -
        2 s
        -
        Download, about 9 s
        -
        Upload, about 9 s
        -
        -
        -

        Connect

        MeterX opens a connection to the nearest Cloudflare location, or to your own server. Under a second.

        -

        Ping and jitter

        A run of small pings measures the delay and how much it wobbles from one ping to the next.

        -

        Download

        Four parallel streams fill your line for a fixed time. The slow start and the tail are trimmed, so the figure is the steady speed.

        -

        Upload

        Three streams do the same upward, while pings keep running to catch the delay that builds under load.

        -
        -
        - -
        -
        -

        Install MeterX in Firefox, Microsoft Edge, Opera, and soon Chrome.

        -
        -

        One link for any browser, ready to share: meterx.ajithakdev.com/get

        +
        +

        MeterX is a free, open-source browser extension that tests download, upload, ping, jitter and bufferbloat from your toolbar. No account, nothing tracked.

        + -
        -
        +
        -
        -
        -

        Questions, answered

        -
        -
        Is it free?

        Yes, with no paid tier, no account and no ads. The code is open source under AGPL-3.0.

        -
        What leaves my browser?

        Only the test traffic itself: bytes downloaded from and uploaded to the test server, and the pings. Results are stored in your browser and nowhere else. The server keeps no logs. The privacy page has the details.

        -
        Why is my result different from other speed tests?

        Each test uses its own servers, number of streams and length. MeterX tests against the nearest Cloudflare location with several streams for a fixed time and trims the slow start, which is close to how your browser loads real pages and downloads.

        -
        What is bufferbloat?

        When a line is busy, routers can queue too much data, so every ping waits in line. Speed looks fine but calls and games lag. MeterX measures ping while the line is full and grades how much it rises.

        -
        Can I use my own test server?

        Yes. The repository includes a small server you can run on your own machine or network, and the settings page lets you point MeterX at it.

        -
        When is it coming to Chrome?

        The Chrome Web Store listing is planned. Until then MeterX is available for Firefox, Edge and Opera.

        +
        +
        +
        + +
        The real side panel with sample data. Try the tabs, Run test and Export.
        +
        +
        +

        Every test, kept on your device

        +

        The side panel keeps up to 200 results with a trend chart, so a slow evening stands out from a slow line.

        +
          +
        • Against your planEnter the speeds you pay for and see what share of them arrives.
        • +
        • Reports you can hand overA PDF with averages and a trend, or CSV and JSON with units in every column.
        • +
        • Tests on a timerEvery 30 minutes, 2 hours or once a day, with a notice when quality drops.
        • +
        +

        Everything MeterX does

        -
        -

        Test from your toolbar.

        -
        -

        Click the icon, read the answer, and keep every result. Free, open source, no account.

        - Get MeterX, free +
        +
        +

        Mbps do?

        +
          +

          Counts use the extension's own rules: 25 Mbps for each 4K stream and 5 Mbps for HD.

          +
          + Get MeterX, free + +
          +

          A five second check from this page fills in your own speed. It uses up to 25 MB.

          -
          - - + + + diff --git a/apps/site/static/privacy/index.html b/apps/site/static/privacy/index.html index 3ebe33b..91d9b2a 100644 --- a/apps/site/static/privacy/index.html +++ b/apps/site/static/privacy/index.html @@ -18,17 +18,10 @@ - - +
          -

          Privacy policy

          MeterX collects no personal data. Your results stay in your browser.

          Last updated 24 October 2026. Applies to MeterX 2027.0.0 and later.

          +

          Privacy policy

          MeterX collects no personal data. Your results stay in your browser.

          Last updated 24 October 2026. Applies to MeterX 2027.0.0 and later.

          What the extension stores

          - - + diff --git a/apps/site/static/r/index.html b/apps/site/static/r/index.html index b81ba6a..3de39ef 100644 --- a/apps/site/static/r/index.html +++ b/apps/site/static/r/index.html @@ -18,18 +18,11 @@ - - +
          - - + diff --git a/apps/site/static/styles/home.css b/apps/site/static/styles/home.css new file mode 100644 index 0000000..a3323f4 --- /dev/null +++ b/apps/site/static/styles/home.css @@ -0,0 +1,47 @@ +/* Home: the pinned hero with the real popup, the live side panel, and the closing question. */ + +/* The stage pins at the top of the window, so it leaves room for the sticky header. */ +.hero-stage { display: grid; align-items: center; padding: var(--bar-h) var(--m) 0; } +.hero .split { width: 100%; } +.hero h1 { font-size: var(--t-h1); } +.hero .words { gap: 24px; } +.actions { display: flex; flex-wrap: wrap; gap: 12px; } + +/* The phase log: each step lights as the sample test reaches it. Fixed rows, so nothing moves. */ +.phase-log { display: grid; gap: 2px; max-width: 420px; } +.phase-log li { display: flex; justify-content: space-between; gap: 16px; align-items: baseline; padding: 8px 12px; border-radius: 8px; color: var(--muted); transition: color 200ms var(--ease), background-color 200ms var(--ease); } +.phase-log li b { font-weight: 500; font-variant-numeric: tabular-nums; min-width: 9ch; text-align: right; color: var(--text); } +.phase-log li[data-state="active"] { background: var(--raised); color: var(--text); } +.phase-log li[data-state="active"] b { color: var(--data); } +.phase-log li[data-state="done"] { color: var(--text); } + +.device-wrap { margin: 0; } +.hero-after { display: none; } + +@media (max-width: 900px) { + .hero-stage { align-items: start; padding-top: calc(var(--bar-h) + 16px); } + .hero h1 { font-size: clamp(32px, 9vw, 44px); } + .hero .lede, .hero .phase-log, .hero .actions { display: none; } + .hero .split { gap: 16px; } + /* The popup keeps its real size inside, shown at 80% so it fits a phone screen with the headline. */ + .hero .device-wrap { width: 288px; height: 476px; margin: 0 auto; } + .hero .device.popup { width: 360px; max-width: none; margin: 0; transform: scale(.8); transform-origin: top left; } + .hero .demo-note { display: none; } + .hero-after { display: grid; gap: 20px; margin-top: 32px; } + .hero-after .actions > * { flex: 1 1 100%; } +} + +/* The close: the heading is an input. */ +.close-in { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 480px); gap: 24px 64px; align-items: start; } +.close-in .ask { grid-column: 1 / -1; } +.close-in .answer-list { grid-column: 2; grid-row: 2 / span 3; } +@media (max-width: 900px) { .close-in { grid-template-columns: 1fr; } .close-in .answer-list { grid-column: 1; grid-row: auto; } } +.ask { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 .25em; font-size: clamp(36px, 5.6vw, 72px) !important; } +.ask input { font: inherit; letter-spacing: inherit; color: var(--data); background: transparent; border: 0; padding: 0; width: 3.4ch; caret-color: var(--data); font-variant-numeric: tabular-nums; border-bottom: 3px solid var(--control); } +.ask input:focus-visible { outline: none; border-bottom-color: var(--data); } +.answer-list.clean { width: 100%; padding: 4px 20px; } +.answer-list li { display: flex; justify-content: space-between; gap: 16px; padding: 14px 0; font-size: 17px; } +.answer-list li + li { border-top: 1px solid var(--rule); } +.answer-list .v { font-weight: 500; font-variant-numeric: tabular-nums; } +.qt { min-height: 3.2em; } +.qt b { color: var(--data); font-weight: 600; } diff --git a/apps/site/static/styles/site.css b/apps/site/static/styles/site.css index 946c333..9975727 100644 --- a/apps/site/static/styles/site.css +++ b/apps/site/static/styles/site.css @@ -1,190 +1,158 @@ /* - * MeterX site. Loaded after the extension's app.css, which supplies the colour tokens (light and dark), - * the Geist face and the components that interface fragments ([data-ui]) are built from. - * Five type steps: display, title, statement, body, small. + * MeterX site. Loaded after the extension's app.css, which supplies the colour tokens (light and dark) + * and the Geist face, so the site and the product always share one palette. + * Spacing steps: 4, 8, 12, 16, 24, 32, 48, 64, 96, 128. */ :root { - /* The one pair of outer margins: 32px, growing on wide screens so content stays in a centred 1120px column. */ - --m: max(32px, calc((100vw - 1120px) / 2)); - --t-display: clamp(40px, 3.9vw, 64px); - --t-title: clamp(34px, 4.6vw, 72px); - --t-statement: clamp(22px, 2vw, 30px); - --t-body: 18px; - --t-small: 15px; - --gap-section: clamp(120px, 14vw, 200px); - --round: 999px; + --page: 1120px; + --m: max(20px, calc((100vw - var(--page)) / 2)); --on-data: #ffffff; + --round: 999px; + --t-h1: clamp(40px, 5.2vw, 68px); + --t-h2: clamp(30px, 3.4vw, 44px); + --t-h3: 20px; + --t-lede: 20px; + --t-body: 17px; + --t-small: 14px; + --bar-h: 68px; } +@media (max-width: 760px) { :root { --bar-h: 108px; } } @media (prefers-color-scheme: dark) { :root { --on-data: #0b0c0e; } } -@media (max-width: 720px) { :root { --m: 16px; --t-body: 17px; } } html { scroll-behavior: smooth; } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } } -body.site { font: 400 var(--t-body)/1.55 var(--font); background: var(--ground); color: var(--text); } -.site a:not(.cta) { color: inherit; } -.site main > section { padding: 0 var(--m); margin-top: var(--gap-section); } -.site main > section.hero { margin-top: 0; } -.wrap { max-width: 1600px; margin: 0 auto; } -.site p { max-width: 62ch; } -.site h1, .site h2 { font-weight: 600; letter-spacing: -0.035em; line-height: 0.98; margin: 0; } -.site h2 { font-size: var(--t-title); max-width: 18ch; } +body.site { font: 400 var(--t-body)/1.6 var(--font); background: var(--ground); color: var(--text); margin: 0; } +.site h1, .site h2, .site h3 { margin: 0; font-weight: 600; letter-spacing: -0.03em; line-height: 1.05; } +.site h1 { font-size: var(--t-h1); } +.site h2 { font-size: var(--t-h2); } +.site h3 { font-size: var(--t-h3); letter-spacing: -0.01em; line-height: 1.3; } +.site p { margin: 0; max-width: 62ch; } +.lede { font-size: var(--t-lede); line-height: 1.55; color: var(--muted); } .small { font-size: var(--t-small); color: var(--muted); } .nowrap { white-space: nowrap; } +.textlink { color: inherit; text-underline-offset: 4px; text-decoration-thickness: 1px; } +.textlink:hover { text-decoration-thickness: 2px; } +.skip { position: absolute; left: -9999px; } +.skip:focus { left: 16px; top: 12px; z-index: 50; background: var(--surface); padding: 8px 12px; border-radius: 6px; } -/* Buttons: one shape, fully round, one fill. */ -.cta { - display: inline-flex; align-items: center; justify-content: center; height: 52px; padding: 0 26px; border-radius: var(--round); - background: var(--data); color: var(--on-data); font-weight: 500; font-size: 17px; text-decoration: none; white-space: nowrap; border: 0; cursor: pointer; +/* Buttons: fully round, one fill for the action, a quiet fill for the rest. */ +.cta, .ghost { + display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 48px; padding: 0 24px; + border-radius: var(--round); border: 0; font: inherit; font-size: 16px; font-weight: 500; text-decoration: none; white-space: nowrap; cursor: pointer; transition: transform 120ms var(--ease), background-color 160ms var(--ease); } +.cta { background: var(--data); color: var(--on-data); } .cta:hover { background: color-mix(in srgb, var(--data) 88%, var(--text)); } -.cta:active { transform: scale(.97); } -.cta.small-cta { height: 40px; padding: 0 18px; font-size: 15px; } -.textlink { text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 1px; } -.textlink:hover { text-decoration-thickness: 2px; } - -/* Navigation: wordmark, three links, one action. */ -.nav { display: flex; align-items: center; gap: 28px; height: 76px; padding: 0 var(--m); } -.nav .brand { display: inline-flex; align-items: center; gap: 10px; font-weight: 600; font-size: 18px; text-decoration: none; margin-right: auto; } -.nav .brand img { width: 30px; height: 30px; border-radius: 8px; } -.nav a:not(.brand):not(.cta) { text-decoration: none; font-size: var(--t-small); } -.nav a:not(.brand):not(.cta):hover { text-decoration: underline; text-underline-offset: 4px; } -@media (max-width: 720px) { .nav { gap: 16px; height: 64px; } .nav .hide-sm { display: none; } } - -/* Hero: what it is and the two actions on the left, the real popup on the right. */ -.hero { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 64px; align-items: center; padding-top: 56px !important; padding-bottom: 24px !important; } -.hero h1 { font-size: clamp(32px, 5vw, 64px); max-width: none; } -.hero .lede { font-size: 20px; line-height: 1.5; margin: 28px 0 0; max-width: 44ch; } -.hero .actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 36px; } -.ghost { - height: 52px; padding: 0 24px; border-radius: var(--round); border: 0; background: var(--raised); color: var(--text); - font: inherit; font-size: 17px; font-weight: 500; cursor: pointer; transition: transform 120ms var(--ease), background-color 160ms var(--ease); -} +.ghost { background: var(--raised); color: var(--text); } .ghost:hover { background: var(--control); } -.ghost:active { transform: scale(.97); } -.ghost:disabled { cursor: progress; opacity: .7; } -/* One reserved line, so a result never moves anything. */ -.hero .qt { margin: 16px 0 0; min-height: 3.2em; line-height: 1.6; font-size: var(--t-small); color: var(--muted); } -.hero .qt b { color: var(--data); font-weight: 600; font-size: 18px; } -.hero-shot img { display: block; width: 360px; height: auto; border-radius: 16px; } -@media (max-width: 900px) { - .hero { grid-template-columns: 1fr; gap: 40px; padding-top: 24px !important; } - .hero-shot img { width: min(360px, 100%); margin: 0 auto; } - .hero .actions > * { flex: 1 1 100%; } +.cta:active, .ghost:active { transform: scale(.97); } +.cta-sm { height: 40px; padding: 0 18px; font-size: 15px; } + +/* Header: the brand, four pages, the one action. On phones the links become a scrolling row. */ +.site-bar { position: sticky; top: 0; z-index: 40; background: var(--ground); border-bottom: 1px solid transparent; transition: border-color 160ms var(--ease); } +.site-bar.is-scrolled { border-bottom-color: var(--border); } +.site-bar .in { display: flex; align-items: center; gap: 32px; height: 68px; padding: 0 var(--m); } +.brand { display: inline-flex; align-items: center; gap: 10px; font-weight: 600; font-size: 18px; color: var(--text); text-decoration: none; } +.brand img { border-radius: 7px; } +.links { display: flex; gap: 4px; margin-right: auto; } +.links a { padding: 8px 12px; border-radius: 8px; color: var(--muted); text-decoration: none; font-size: 15px; transition: color 120ms var(--ease), background-color 120ms var(--ease); } +.links a:hover { color: var(--text); background: var(--raised); } +.links a[aria-current="page"] { color: var(--text); background: var(--raised); } +@media (max-width: 760px) { + .site-bar .in { flex-wrap: wrap; height: auto; gap: 0 12px; padding-top: 10px; } + .brand { margin-right: auto; height: 44px; } + .links { order: 3; flex-basis: 100%; overflow-x: auto; margin: 0 calc(-1 * var(--m)); padding: 4px var(--m) 8px; scrollbar-width: none; } + .links a { white-space: nowrap; } } -/* Result card (share viewer): real app components at real size. */ -[data-ui] { font-size: 14px; line-height: 20px; } -.frame.card { padding: 24px 28px; display: grid; gap: 14px; } -.frame .row { display: flex; align-items: center; justify-content: space-between; gap: 12px; } -.frame .label { color: var(--muted); font-size: 14px; } -.frame .fig { font-size: 72px; line-height: 1; font-weight: 500; letter-spacing: -0.02em; color: var(--data); font-variant-numeric: tabular-nums; } -.frame .fig.dash { color: transparent; } - -/* What your speed can do: the heading is an input. */ -.explorer h2 { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 .25em; max-width: none; font-size: clamp(44px, 6vw, 88px); } -.explorer input { - font: inherit; letter-spacing: inherit; color: var(--data); background: transparent; border: 0; padding: 0; margin: 0; - width: 3.4ch; caret-color: var(--data); font-variant-numeric: tabular-nums; -} -.explorer input:focus-visible { outline: none; text-decoration: underline; text-decoration-thickness: 4px; text-underline-offset: 10px; } -.explorer .answer { display: grid; grid-template-columns: minmax(0, 620px) minmax(0, 1fr); gap: 64px; margin-top: 48px; align-items: end; } -.explorer .aside { display: grid; gap: 24px; } -.explorer .aside p { margin: 0; } -.explorer .list { padding: 6px 20px; } -.explorer .list li { display: flex; justify-content: space-between; gap: 16px; padding: 14px 0; font-size: 18px; } -.explorer .list li + li { border-top: 1px solid var(--rule); } /* taste-skill-ignore: rows of a data-ui list, as in the app */ -.explorer .list .v { font-weight: 500; font-variant-numeric: tabular-nums; } -.explorer .tries { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; } -.explorer .tries button { height: 44px; padding: 0 18px; border-radius: var(--round); border: 0; background: var(--raised); color: var(--text); font: inherit; font-size: var(--t-small); cursor: pointer; transition: transform 120ms var(--ease); } -.explorer .tries button:hover { background: var(--control); } -.explorer .tries button:active { transform: scale(.96); } -@media (max-width: 900px) { .explorer .answer { grid-template-columns: 1fr; gap: 24px; } } - -/* Features: rows hanging from one axis, each with its interface fragment at the right. */ -.features .axis { display: grid; grid-template-columns: 1fr; } -.features h2 { margin-bottom: 80px; } -.feature { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: 64px; align-items: center; } -.feature + .feature { margin-top: 112px; } -.feature h3 { font-size: var(--t-statement); line-height: 1.15; letter-spacing: -0.02em; font-weight: 600; margin: 0 0 12px; } -.feature p { margin: 0; max-width: 44ch; } -.feature .shot { width: 100%; max-width: 520px; justify-self: end; } -.feature .shot.narrow { max-width: 440px; } -.feature .shot img { display: block; width: 100%; height: auto; border-radius: var(--radius-card); } -@media (max-width: 1000px) { - .features .axis { grid-template-columns: 1fr; } - .features h2, .feature { grid-column: 1; } - .feature { grid-template-columns: 1fr; gap: 24px; } - .feature .shot, .feature .shot.wide, .feature .shot.narrow { width: 100%; max-width: 420px; } - .feature:nth-child(odd) .shot { justify-self: end; } +/* Footer: one calm row. */ +.site-foot { margin-top: 128px; border-top: 1px solid var(--border); } +.site-foot .in { display: flex; flex-wrap: wrap; align-items: center; gap: 16px 32px; padding: 32px var(--m) 40px; } +.site-foot nav { display: flex; flex-wrap: wrap; gap: 8px 20px; margin-right: auto; } +.site-foot nav a, .site-foot p { color: var(--muted); font-size: var(--t-small); text-decoration: none; } +.site-foot nav a:hover { color: var(--text); } +.site-foot .brand { font-size: 16px; } + +/* Page frame */ +main { display: block; } +.wrap { padding: 0 var(--m); } +.page-head { padding: 72px var(--m) 0; display: grid; gap: 20px; max-width: calc(var(--page) + 2 * var(--m)); } +.page-head .lede { max-width: 56ch; } +.section { padding: 0 var(--m); margin-top: 120px; } +.section > .head { display: grid; gap: 12px; margin-bottom: 40px; max-width: 64ch; } +@media (max-width: 760px) { .page-head { padding-top: 40px; } .section { margin-top: 88px; } } + +/* A real extension page shown in a frame. */ +.device { border: 0; display: block; background: var(--ground); border-radius: 14px; box-shadow: 0 0 0 1px var(--border), 0 24px 48px -24px rgb(0 0 0 / .35); color-scheme: light dark; } +.device.popup { width: 360px; height: 594px; } +.device.panel { width: 420px; height: 760px; } +.device.options { width: 100%; height: 660px; } +@media (max-width: 900px) { .device.options { height: 1500px; } } +.demo-note { margin-top: 12px; font-size: var(--t-small); color: var(--muted); } + +/* Split: words on one side, the product on the other. */ +.split { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 64px; align-items: center; } +.split.flip { grid-template-columns: auto minmax(0, 1fr); } +.split .words { display: grid; gap: 20px; align-content: start; } +@media (max-width: 900px) { + .split, .split.flip { grid-template-columns: 1fr; gap: 40px; } + .split.flip > :first-child { order: 2; } + .device.popup, .device.panel { margin: 0 auto; max-width: 100%; } } -/* How it works: the progress bar enlarged, segments sized by real phase length, four steps under it. */ -.how h2 { margin-bottom: 48px; max-width: none; } -.phase-bar { display: flex; gap: 6px; height: 72px; } +/* Lists of facts, with numbers in the product's own figure style. */ +.facts { display: grid; gap: 24px; } +.facts li { display: grid; gap: 4px; } +.facts b { font-weight: 600; } +ul.clean { list-style: none; margin: 0; padding: 0; } + +/* Documents: about, privacy, share, get, 404. */ +.doc .body { padding: 0 var(--m); margin-top: 64px; display: grid; gap: 16px; max-width: calc(68ch + 2 * var(--m)); } +.doc .body h2 { font-size: 24px; letter-spacing: -0.02em; margin-top: 40px; } +.doc .body h2:first-child { margin-top: 0; } +.doc ul { padding-left: 1.2em; display: grid; gap: 6px; max-width: 62ch; } + +/* Entrances: parts rise in once. Nothing is hidden without script or with reduced motion. */ +.pre { opacity: 0; transform: translateY(20px); } +[data-enter] > * { transition: opacity 600ms var(--ease) calc(var(--i, 0) * 70ms), transform 600ms var(--ease) calc(var(--i, 0) * 70ms); } +@media (prefers-reduced-motion: reduce) { .pre { opacity: 1; transform: none; } } + +/* How it works: the progress bar enlarged, segments sized by real phase length. */ +.phase-bar { display: flex; gap: 6px; height: 64px; } .phase-bar > div { border-radius: 6px; background: var(--raised); display: flex; align-items: center; padding: 0 16px; min-width: 10px; overflow: hidden; white-space: nowrap; font-size: var(--t-small); } .phase-bar > .dl { background: var(--data); color: var(--on-data); } -.how .steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 32px; margin-top: 40px; } -.how h3 { font-size: 20px; font-weight: 600; letter-spacing: -0.01em; margin: 0 0 8px; } -.how .steps p { margin: 0; font-size: 17px; } -@media (max-width: 900px) { .how .steps { grid-template-columns: 1fr 1fr; } } -@media (max-width: 560px) { .how .steps { grid-template-columns: 1fr; } .phase-bar > div { padding: 0 8px; } .phase-bar span { display: none; } } - -/* Where it runs: the browser names as one sentence, one lit at a time. */ -.where .wrap { display: grid; grid-template-columns: 8fr 4fr; column-gap: 64px; align-items: end; } -.where .side p { margin: 0 0 24px; } -.names { font-size: 36px; font-weight: 600; letter-spacing: -0.025em; line-height: 1.2; max-width: 26ch; margin: 0; } -@media (max-width: 900px) { .where .wrap { grid-template-columns: 1fr; row-gap: 32px; } .names { font-size: 24px; line-height: 1.3; letter-spacing: -0.01em; } } -.names .name { color: color-mix(in srgb, var(--text) 52%, var(--ground)); transition: color 400ms var(--ease); text-decoration: none; } -.names .name.lit, .names a.name:hover { color: var(--text); } -.names .soon { color: color-mix(in srgb, var(--text) 52%, var(--ground)); } - -/* Questions: a wide heading, questions indented under it. */ -.faq .qs { margin: 80px 0 0 calc(33.3% + 11px); } -.faq details { border-radius: 8px; padding: 0 24px; margin: 0 -24px; transition: background-color 160ms var(--ease); } -.faq details + details { margin-top: 12px; } -.faq details[open] { background: var(--raised); padding-bottom: 24px; } -.faq summary { list-style: none; cursor: pointer; display: flex; justify-content: space-between; gap: 24px; align-items: center; font-size: 23px; letter-spacing: -0.01em; padding: 22px 0; } +.phase-bar > .ul { background: var(--control); } +.steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 32px; margin-top: 32px; } +.steps li { display: grid; gap: 8px; align-content: start; } +@media (max-width: 900px) { .steps { grid-template-columns: 1fr 1fr; } } +@media (max-width: 560px) { .steps { grid-template-columns: 1fr; } .phase-bar span { display: none; } } + +.grades { width: 100%; max-width: 760px; border-collapse: collapse; margin-top: 32px; } +.grades caption { text-align: left; padding-bottom: 12px; } +.grades th { text-align: left; font-weight: 500; color: var(--muted); font-size: var(--t-small); padding: 0 16px 10px 0; } +.grades td { padding: 12px 16px 12px 0; border-top: 1px solid var(--rule); vertical-align: middle; } +.grades td:first-child { width: 72px; } + +.facts.two { grid-template-columns: 1fr 1fr; gap: 32px 48px; } +@media (max-width: 760px) { .facts.two { grid-template-columns: 1fr; } } +.facts li > span { color: var(--muted); } + +/* Questions */ +.faq { display: grid; gap: 8px; max-width: calc(760px + 2 * var(--m)); margin-top: 56px; } +.faq details { border-radius: 10px; padding: 0 20px; background: var(--surface); box-shadow: 0 0 0 1px var(--border); } +.faq summary { list-style: none; cursor: pointer; display: flex; justify-content: space-between; gap: 24px; align-items: center; font-size: 18px; font-weight: 500; padding: 18px 0; } .faq summary::-webkit-details-marker { display: none; } -.faq summary::after { content: ""; width: 10px; height: 10px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: rotate(45deg) translateY(-3px); flex: none; transition: transform 160ms var(--ease); } +.faq summary::after { content: ""; width: 9px; height: 9px; border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted); transform: rotate(45deg) translateY(-3px); flex: none; transition: transform 160ms var(--ease); } .faq details[open] summary::after { transform: rotate(225deg) translateY(-3px); } -.faq details p { margin: 0; font-size: var(--t-body); max-width: 54ch; } -@media (max-width: 900px) { .faq .qs { margin: 40px 0 0; } .faq summary { font-size: 19px; } } - -/* Close: the page's colours inverted. */ -.close { background: var(--text); color: var(--ground); display: grid; grid-template-columns: 7fr 5fr; column-gap: 64px; align-items: end; padding: 200px var(--m) 120px; margin-top: var(--gap-section); } -.close h2 { font-size: clamp(44px, 6vw, 88px); line-height: 0.92; max-width: 12ch; } -.close .row { display: grid; gap: 28px; justify-items: start; } -.close p { font-size: var(--t-body); margin: 0; max-width: 40ch; } -@media (max-width: 900px) { .close { grid-template-columns: 1fr; row-gap: 48px; padding: 96px var(--m) 72px; } .close .cta { width: 100%; } } - -/* Footer: the repository is the display line. */ -.footer { background: var(--text); color: var(--ground); padding: 0 var(--m) 32px; } -.footer .top { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 16px; font-size: var(--t-small); padding-top: 64px; } -.footer .top p { margin: 0; } -.footer .big { display: block; font-size: clamp(30px, 5vw, 96px); font-weight: 600; letter-spacing: -0.04em; line-height: 1.02; text-decoration: none; margin: 120px 0 40px; } -.footer .big:hover { text-decoration: underline; text-decoration-thickness: 3px; text-underline-offset: 8px; } -.footer .meta { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px 24px; font-size: 14px; color: color-mix(in srgb, var(--ground) 70%, var(--text)); } -.footer .meta nav { display: flex; flex-wrap: wrap; gap: 18px; } - -/* Plain document pages: about, privacy, share, get. */ -.doc { padding: 40px var(--m) 0; } -.doc .head { max-width: 900px; } -.doc h1 { font-size: var(--t-title); } -.doc .lede { font-size: var(--t-statement); line-height: 1.35; margin: 24px 0 0; max-width: 40ch; } -.doc .body { display: grid; grid-template-columns: 4fr 8fr; column-gap: 32px; margin-top: 96px; } -.doc .body > * { grid-column: 2; } -.doc h2 { font-size: var(--t-statement); line-height: 1.2; letter-spacing: -0.02em; max-width: none; margin: 56px 0 12px; } -.doc h2:first-child { margin-top: 0; } -.doc ul { list-style: disc; padding-left: 1.2em; max-width: 62ch; } -.doc li + li { margin-top: 6px; } -@media (max-width: 900px) { .doc .body { grid-template-columns: 1fr; margin-top: 56px; } .doc .body > * { grid-column: 1; } } - -/* Entrances: parts rise in once; without script or with reduced motion nothing is hidden. */ -.pre { opacity: 0; transform: translateY(24px); } -[data-enter] > * { transition: opacity 600ms var(--ease) calc(var(--i, 0) * 80ms), transform 600ms var(--ease) calc(var(--i, 0) * 80ms); } -@media (prefers-reduced-motion: reduce) { .pre { opacity: 1; transform: none; } } +.faq details p { padding: 0 0 20px; color: var(--muted); } -.skip { position: absolute; left: -9999px; } -.skip:focus { left: var(--m); top: 12px; z-index: 10; background: var(--surface); padding: 8px 12px; border-radius: 6px; } +/* A full report page as a picture: the whole page, never cropped. */ +.device-wrap.report { width: min(440px, 100%); } +.device-wrap.report img { display: block; width: 100%; height: auto; border-radius: 10px; box-shadow: 0 0 0 1px var(--border), 0 24px 48px -24px rgb(0 0 0 / .35); } + +/* Share viewer result card */ +.frame.card { padding: 24px; display: grid; gap: 14px; max-width: 520px; } +.frame .row { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.frame .label { color: var(--muted); font-size: 14px; } +.frame .fig { font-size: 64px; line-height: 1; font-weight: 500; letter-spacing: -0.02em; color: var(--data); font-variant-numeric: tabular-nums; } From 0b1cc20eebe5e8e34d4e97ffa8ad87ea30b0f58c Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Sat, 24 Oct 2026 15:06:33 +0530 Subject: [PATCH 20/44] docs: architecture, code map generator, README, guide and SEO for the new layout CLAUDE.md and the code map describe apps, services and packages; scripts/codemap.mjs regenerates the file table. The worker URL lives once in @meterx/shared. README, RESOURCES-AND-GUIDE and the privacy pointer match the renamed repo, the site and the store packaging, and docs/SEO.md covers the site. --- CHANGELOG.md | 4 + CLAUDE.md | 55 +++++----- README.md | 128 +++++++--------------- RESOURCES-AND-GUIDE.md | 92 +++++++++------- apps/extension/src/lib/browser.ts | 3 +- apps/extension/src/pages/options.ts | 4 +- apps/site/src/quick-test.ts | 2 +- apps/site/static/index.html | 1 + apps/site/static/robots.txt | 2 + apps/site/static/sitemap.xml | 3 + docs/CODEMAP.md | 160 +++++++++++++--------------- docs/SEO.md | 49 +++++++++ docs/SITE-PLAN.md | 2 +- docs/STATE.md | 30 +++--- packages/shared/src/index.ts | 3 + privacy-policy.md | 67 +----------- scripts/codemap.mjs | 40 +++++++ 17 files changed, 317 insertions(+), 328 deletions(-) create mode 100644 docs/SEO.md create mode 100644 scripts/codemap.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index c629d59..0d6217b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ All notable changes to MeterX are documented here. The format follows [Keep a Ch - Product site in `apps/site` for meterx.ajithakdev.com: home page with an in-page quick test, about, privacy policy (#92), share viewer (#74) and a `/get` link that opens the right store - The worker answers the site's origin so its quick test can run +- Repository renamed to `ajithakdev/meterx` and split into `apps/`, `services/` and `packages/`; the PDF, CSV and JSON builders became `@meterx/reports` +- `npm run package` builds one upload for Chrome, Edge and Opera, one for Firefox, and a source archive +- The site is now several pages (Home, How it works, Features, Questions, About) and runs the extension's own popup, side panel and settings with sample data; the home page plays a test as you scroll + ### Fixed - Empty figures show a short muted bar with the unit hidden, instead of a stray hyphen next to "Mbps" - The side panel no longer scrolls when an ISP plan is set: plan rows show the percentage on one line, the status moved into the Results header, and the chart shrinks on shorter panels diff --git a/CLAUDE.md b/CLAUDE.md index ce904fd..57360b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,52 +8,55 @@ Before reading code, check `docs/CODEMAP.md` (file map with line counts and the ## Commands -npm workspaces monorepo (`packages/*`). Run from the root unless noted. +npm workspaces monorepo. Run from the root unless noted. ```bash npm install -npm run build # all workspaces: extension -> apps/extension/dist, server -> services/server/dist/server.js -npm test # vitest in extension + server (worker and shared have no tests) +npm run build # every workspace: extension -> apps/extension/dist, site -> apps/site/dist (needs the extension build), server -> services/server/dist +npm test # vitest in every workspace, plus scripts/ npm run lint # eslint (TS, CSS, Markdown) +npm run package # store uploads in release/: one zip for Chrome, Edge and Opera, one for Firefox, and a source zip npm start # self-host Express server on :3000 (needs a prior build) -# Single test file / test name -cd services/server && npx vitest run src/app.test.ts -cd apps/extension && npx vitest run src/quality.test.ts -t "excellent" +# One package +cd apps/extension && npx vitest run src/lib/quality.test.ts -t "excellent" +cd packages/reports && npx tsc --noEmit -# Type check (CI does this for server and extension only) -cd services/server && npx tsc --noEmit -cd apps/extension && npx tsc --noEmit - -# Worker -cd services/worker && npx wrangler dev # local -cd services/worker && npx wrangler deploy # deploy to Cloudflare (free plan only) +# Worker and site deploys (free plans only) +cd services/worker && npx wrangler deploy +npm run deploy -w @meterx/site # Cloudflare Pages project meterx-site ``` -Load the extension unpacked from `apps/extension/dist/` (rebuild after every change; `static/` is copied verbatim into `dist/`). - -CI (`.github/workflows/ci.yml`) runs lint, both type checks, build and tests on Node 22, and on pushes to `main` uploads `meterx-extension.zip`. +Load the extension unpacked from `apps/extension/dist/` (rebuild after every change; `static/` is copied verbatim into `dist/`). Preview the site with `npx wrangler pages dev apps/site/dist`. ## Architecture -Three runtimes share one contract: +``` +apps/extension Manifest V3 extension (popup, side panel, settings, service worker) +apps/site meterx.ajithakdev.com: static pages plus the extension's own pages in demo mode under /app/ +services/worker Cloudflare Worker, the default test server (streams bytes, stores nothing) +services/server Express self-host server with the same API +packages/shared the contract: types, API constants, version, quality rules, formatting, share links (no browser APIs) +packages/reports PDF, CSV and JSON builders and their stats (pure; used by the extension, testable in Node) +brand/ logo masters and build.mjs, which makes every icon, store image and social card +scripts/ package-stores.mjs for store uploads +``` -- **`apps/extension`**: Manifest V3 extension, bundled by `build.mjs` (esbuild, ESM, 4 entry points: `background`, `popup`, `options`, `sidepanel`). All measurement logic lives in `src/background.ts` (service worker). The UI pages are thin: they send `chrome.runtime` messages (`startTest`, `cancelTest`, `getServerUrl`, `setServerUrl`, `rescheduleTests`, `openSidePanel`) and listen for `testProgress` / `testComplete` / `testError` broadcasts. Helpers (`quality`, `history`, `isp`, `share`, `chart`, `sparkline`, `export-*`) are imported by the pages. -- **`services/worker`**: Cloudflare Worker, the default backend (`DEFAULT_SERVER_URL` in `background.ts` and `DEFAULT_URL` in `options.ts`, plus `host_permissions` in `static/manifest.json`; change all three together). Streams generated bytes, no storage. -- **`services/server`**: Express 5 self-host fallback. `app.ts` exports the app (tested with supertest in `app.test.ts`); `server.ts` only listens. Generates zero-filled `test-files/*.bin` on demand. CORS allows extension origins plus `CORS_ORIGINS` (comma list). `src/test.ts` is a legacy CommonJS smoke script, not part of `npm test`. -- **`packages/shared`**: types and thresholds only (`QUALITY_THRESHOLDS`, `USE_CASE_REQUIREMENTS`, `TestResults`, `HistoryEntry`, ...), consumed as raw TS (`main: ./src/index.ts`) and bundled into each package. +- **Extension** (`apps/extension`): esbuild bundles `src/background/index.ts` (service worker; measurement in `background/runner.ts` and `measure.ts`) and `src/pages/{popup,sidepanel,options}.ts`. Pages are thin: they send `chrome.runtime` messages (`startTest`, `cancelTest`, `getState`, `rescheduleTests`) and listen for `testProgress` / `testComplete` / `testError`. Browser-only helpers live in `src/lib/` (history, isp, export downloads, ui, tooltips, browser). +- **Site** (`apps/site`): `build.mjs` copies `static/`, fills the `` and `` partials, and copies the built extension pages into `dist/app/` with `src/demo-shim.ts` injected before their scripts. The shim stands in for the extension APIs with labelled sample data (`src/sample.ts`); the home page drives the popup's sample test from scroll through `postMessage`. The scroll engine is `static/sc/` (scroll-craft, unmodified). +- **Worker and server**: same API, kept in sync. CORS answers extension origins, the site origin and localhost dev ports (`isAllowedOrigin` in shared). -**Server API contract** (worker and Express must stay in sync): `GET/HEAD /ping`, `GET /test-file/{1|5|10|25}MB.bin`, `POST /upload` (`application/octet-stream`, body discarded), `GET /health`, `GET /ip`. Allowed sizes are duplicated in `worker/src/index.ts` (`ALLOWED_SIZES`) and `server/src/app.ts` (`ALLOWED_FILE_SIZES`); the extension always downloads the 25 MB file. Responses must send `Timing-Allow-Origin` so the extension can read DNS timing. +**Server API contract**: `GET/HEAD /ping`, `GET /test-file/{1|5|10|25}MB.bin`, `POST /upload` (`application/octet-stream`, body discarded), `GET /health`, `GET /ip`. Sizes come from `ALLOWED_SIZES_MB` / `TEST_FILE_RE` in shared. Responses send `Timing-Allow-Origin` so the extension can read DNS timing. -**Measurement** (`background.ts`): parallel streams (4 download, 3 upload) for a fixed duration (`MEASURE_DURATION_MS`), with ramp-up and tail trimmed from the throughput samples, plus loaded-latency pings during load for bufferbloat. The server rate limits in `app.ts` are sized for this burst pattern; raise them if you add streams. +**Measurement**: 4 download and 3 upload streams for a fixed time, ramp-up and tail trimmed, loaded-latency pings for the bufferbloat grade. The rate limits in the worker and server are sized for this burst; raise them if you add streams. -**Storage**: `chrome.storage.sync` holds settings (`serverUrl`, `ispPlan`, `scheduleMinutes`, `notifyOnDrop`); `chrome.storage.local` holds `history` (capped at `MAX_HISTORY` = 200). Scheduled tests use `chrome.alarms` and are re-registered on install/startup. +**Storage**: `chrome.storage.sync` holds settings (`serverUrl`, `ispPlan`, `scheduleMinutes`, `notifyOnDrop`); `chrome.storage.local` holds `history` (capped at 200). Scheduled tests use `chrome.alarms`. -Custom server URLs need a runtime host-permission grant (`optional_host_permissions`); the options page requests it. +Custom server URLs need a runtime host-permission grant (`optional_host_permissions`); the settings page requests it. ## Gotchas - The version lives only in the root `package.json`. `@meterx/shared` exports it as `VERSION` (used by `/health`, the exports and the settings footer), and `build.mjs` stamps it into `dist/manifest.json`, so it must be 1 to 4 dot-separated numbers. Bump that one line to release. - `manifest.json` description must stay within the store's 132-character limit. -- `.junie/guidelines.md` describes the old pre-monorepo JS layout and a Render URL; it is stale, trust the code. +- Store packages: Chromium stores share one zip; Firefox gets a converted manifest (sidebar, background script, id `meterx@ajithakdev`). The default worker URL is `WORKER_URL` in shared, and also sits in `host_permissions` in the extension's `static/manifest.json` and the site's `static/_headers` CSP; change all three together. - License is AGPL-3.0; see `TRADEMARK.md` before touching branding. diff --git a/README.md b/README.md index 8e7f70c..6eaef23 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,63 @@ # MeterX -Every link and how-to is in [RESOURCES-AND-GUIDE.md](RESOURCES-AND-GUIDE.md). +Links, accounts and how-tos: [RESOURCES-AND-GUIDE.md](RESOURCES-AND-GUIDE.md) -[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) -[![CI](https://github.com/ajithakdev/meterx/actions/workflows/ci.yml/badge.svg?branch=add/dev/2026.0.MAR.1)](https://github.com/ajithakdev/meterx/actions) +MeterX is a free, open-source browser extension that tests your internet from the toolbar: download, upload, ping, jitter, packet loss and latency under load (bufferbloat). Results stay in your browser. -Open-source speed test browser extension. Measures download, upload, ping, jitter, and packet loss from your toolbar, powered by Cloudflare's edge network. +Website: [meterx.ajithakdev.com](https://meterx.ajithakdev.com) -## Features +## Install -- **Full speed test**: download, upload, ping, jitter, packet loss -- **Use-case readiness**: labels for Calls, Streaming, Gaming, Uploads -- **Connection quality badge**: live rating on the extension icon -- **PDF health report**: export a full network report as PDF -- **Test history**: last 7 displayed, 200 stored locally -- **Cloudflare edge**: serverless backend at 300+ PoPs worldwide -- **Self-hostable server**: Express.js fallback for LAN or self-hosting -- **Settings page**: configure custom server URL -- **Animated UI**: progress steps, speed bars, smooth transitions -- **No tracking, no accounts**: everything stays in your browser +- Firefox: [addons.mozilla.org](https://addons.mozilla.org/firefox/addon/meterx-by-ajithakdev/) +- Microsoft Edge: [Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/glkpolbkpachomgmmhdkmjnghejbhlfc) +- Opera and Chrome: coming soon -## Install from Source +## What it does -```bash -git clone https://github.com/ajithakdev/meterx.git -cd meterx -npm install -npm run build -``` +- A full test in about twenty seconds, with several parallel streams and the slow start trimmed. +- Latency under load, graded from A to F, so you can see why calls stutter while something downloads. +- A plain answer about what the connection is good enough for: calls, 4K streaming, gaming, uploads. +- History of up to 200 results in the side panel, with a trend chart and a comparison with your ISP plan. +- Scheduled tests with a notice when quality drops. +- PDF reports, and CSV and JSON exports with units in every column. +- Share links that carry the result inside the link, never through a server. +- Tests against the nearest Cloudflare location, or against your own server. -**Load the extension:** -1. Go to `chrome://extensions` (or `edge://extensions`) -2. Enable "Developer mode" -3. Click "Load unpacked" and select `apps/extension/dist/` +## Repository layout -**Run the server locally (optional):** -```bash -npm start ``` - -## Architecture - -```text -meterx/ - packages/ - extension/ # Browser extension (TypeScript, Manifest V3, esbuild) - src/ - background.ts # Service worker: test orchestration - popup.ts # Popup UI, history, PDF export - static/ # HTML, CSS, manifest, icons - build.mjs # esbuild bundler - - worker/ # Cloudflare Worker: primary backend (serverless) - src/index.ts # Edge handler: download, upload, ping, health - wrangler.toml # Wrangler config - - server/ # Express.js server: self-host fallback - src/ - app.ts # Express app (routes, middleware) - server.ts # Entry point - app.test.ts # Vitest + supertest (8 tests) +apps/extension Manifest V3 extension (popup, side panel, settings, service worker) +apps/site meterx.ajithakdev.com, which runs the extension's own pages in demo mode +services/worker Cloudflare Worker, the default test server (stores nothing) +services/server Express server with the same API, for self-hosting +packages/shared types, API constants, version, quality rules, formatting, share links +packages/reports PDF, CSV and JSON builders +brand/ logo and the script that makes every icon and store image +scripts/ store packaging and the code map generator ``` -```text -Browser Extension --> Cloudflare Worker (default, 300+ PoPs) - --> Express Server (self-host / LAN fallback) -``` - -## Server Endpoints - -Both the worker and self-host server expose the same API: - -| Endpoint | Method | Purpose | -|----------|--------|---------| -| `/ping` | GET/HEAD | Latency measurement | -| `/test-file/:size` | GET | Download test (1, 5, 10, 25 MB) | -| `/upload` | POST | Upload drain | -| `/health` | GET | Health check (includes edge location on worker) | - -All endpoints are rate-limited. CORS restricted to browser extensions and configured origins. +More detail in [CLAUDE.md](CLAUDE.md) and [docs/CODEMAP.md](docs/CODEMAP.md). ## Development ```bash -npm install # Install all workspace deps -npm run build # Build all packages -npm start # Run self-host server -npm test # Run all tests -cd services/worker && npx wrangler dev # Run worker locally -npm run lint # ESLint +git clone https://github.com/ajithakdev/meterx.git +cd meterx +npm install +npm run build # extension, site and server +npm test +npm run lint +npm run package # store uploads in release/ ``` -See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide. - -## Tech Stack - -- **Extension:** TypeScript, Manifest V3, esbuild -- **Worker:** Cloudflare Workers, TypeScript, Wrangler -- **Server:** TypeScript, Express.js 5, esbuild -- **Testing:** Vitest, supertest -- **CI:** GitHub Actions +Load the extension unpacked from `apps/extension/dist/`. Preview the site with `npx wrangler pages dev apps/site/dist`. Run the self-host server with `npm start` (port 3000). ## Privacy -MeterX collects zero personal data. All results are stored locally in your browser. Network requests go only to the configured test server. See [privacy-policy.md](privacy-policy.md). +MeterX collects no personal data. Results are stored in your browser, and the default test server keeps no logs. The full policy is at [meterx.ajithakdev.com/privacy](https://meterx.ajithakdev.com/privacy/). ## License -[AGPL-3.0](LICENSE). See [TRADEMARK.md](TRADEMARK.md) for brand usage policy. - -## Author - -**Ajith Kumar K** ([@ajithakdev](https://github.com/ajithakdev)) +[AGPL-3.0](LICENSE). See [TRADEMARK.md](TRADEMARK.md) for the name and logo. -- GitHub: [github.com/ajithakdev](https://github.com/ajithakdev) -- Email: ajithkumarks2579@gmail.com +Made by Ajith Kumar K ([@ajithakdev](https://github.com/ajithakdev)). diff --git a/RESOURCES-AND-GUIDE.md b/RESOURCES-AND-GUIDE.md index c86345b..8448fa7 100644 --- a/RESOURCES-AND-GUIDE.md +++ b/RESOURCES-AND-GUIDE.md @@ -1,14 +1,14 @@ # MeterX: resources and guide -Every link for the speed test extension and server, what it is for, and how to run and release it. Passwords and keys are never written here. +Every link for MeterX, what it is for, and how to run, package and release it. Passwords and keys are never written here. ## TL;DR -- The repo contains the browser extension, Cloudflare Worker backend, and Express fallback server. -- Cloudflare Worker at https://meterx-speedtest.meterx-ajithakdev.workers.dev serves as the default edge backend. -- Run `npm run build` to compile all packages, and `npm test` to run tests. -- Pushes to `main` build the extension and package `meterx-extension.zip` in GitHub Actions CI. -- The extension options page allows switching to a local Express server at http://localhost:3000. +- Website: https://meterx.ajithakdev.com (one link for any browser: https://meterx.ajithakdev.com/get/) +- Code: https://github.com/ajithakdev/meterx (was `meterx-server`; GitHub redirects the old URL) +- `npm run build`, `npm test`, `npm run lint` from the root; `npm run package` writes the store uploads to `release/`. +- Releases are weekly on Sunday: tag, GitHub release, `wrangler deploy` for the worker, store uploads, site deploy. +- Default test server: https://meterx-speedtest.meterx-ajithakdev.workers.dev ## Links and what each one is for @@ -16,63 +16,75 @@ Every link for the speed test extension and server, what it is for, and how to r | Link | Use it for | | --- | --- | -| https://meterx-speedtest.meterx-ajithakdev.workers.dev | Default Cloudflare Worker speed test edge backend | -| https://meterx.dev/share | Shared speed test results viewer | -| https://github.com/ajithakdev | Author GitHub profile | +| https://meterx.ajithakdev.com | Product site: home, how it works, features, questions, about | +| https://meterx.ajithakdev.com/get/ | Sends each visitor to their own browser's store. Use this link in posts and bios | +| https://meterx.ajithakdev.com/privacy/ | Privacy policy (the stores link here) | +| https://meterx.ajithakdev.com/r/ | Share viewer: opens links made by the Share button | +| https://addons.mozilla.org/firefox/addon/meterx-by-ajithakdev/ | Firefox listing (add-on id `meterx@ajithakdev`) | +| https://microsoftedge.microsoft.com/addons/detail/glkpolbkpachomgmmhdkmjnghejbhlfc | Microsoft Edge listing | +| Opera Add-ons | Listing link not found yet; add it to `apps/site/src/stores.ts` when known | +| https://meterx-speedtest.meterx-ajithakdev.workers.dev | Default test server (Cloudflare Worker) | ### Behind the scenes | Link | Use it for | | --- | --- | -| https://github.com/ajithakdev/meterx | Source code, issues, and pull requests | -| https://github.com/ajithakdev/meterx/actions | GitHub Actions CI workflow runs | -| https://github.com/ajithakdev/meterx/issues | Bug reports and issue tracker | +| https://github.com/ajithakdev/meterx | Source code, issues, pull requests, releases | +| https://github.com/ajithakdev/meterx/actions | CI runs | +| https://addons.mozilla.org/developers/ | Firefox uploads and review | +| https://partner.microsoft.com/dashboard/microsoftedge/ | Edge uploads | +| https://addons.opera.com/developer/ | Opera uploads | +| https://dash.cloudflare.com | Worker, Pages project `meterx-site`, DNS for `ajithakdev.com` | ## Accounts | Service | Account | Role | | --- | --- | --- | | GitHub | `ajithakdev` | Owner | -| Cloudflare | `meterx-ajithakdev` | Worker backend host | +| Cloudflare | personal account (`meterx-ajithakdev` workers subdomain) | Worker, Pages, DNS; free plan only | +| Firefox, Edge, Opera developer accounts | Ajith Kumar K | Store listings | ## User guide -Click the MeterX icon in the browser toolbar to open the popup. Select Start Test to measure download speed, upload speed, ping, jitter, and packet loss. Results are saved locally in browser storage. +1. Install MeterX from your browser's store (https://meterx.ajithakdev.com/get/). +2. Click the MeterX icon in the toolbar and press Run test. It takes about twenty seconds. +3. Open the side panel (the panel icon in the popup) to see your history, trend and plan comparison. +4. In settings (the sliders icon), enter your ISP plan, choose a test schedule, or point MeterX at your own server. +5. Export or Share from the popup or side panel to save a PDF, CSV or JSON, or send someone a link. -To point the extension at a self-hosted server, click the settings gear icon in the popup. Select Custom Server under Server. Enter the server URL, such as http://localhost:3000, and click Test to verify the connection. Click Save. Plain http is permitted for localhost, 127.0.0.1, and .local domains. - -## Run and release +## Admin guide | Job | How | | --- | --- | -| Install dependencies | `npm install` | -| Build all packages | `npm run build` | -| Run Express server | `npm start` | -| Run Worker locally | `cd services/worker && npx wrangler dev` | -| Run tests | `npm test` | -| Lint codebase | `npm run lint` | -| Load unpacked extension | Open `chrome://extensions`, enable Developer mode, and load `apps/extension/dist/` | -| Package extension zip | `cd apps/extension/dist && zip -r ../../../meterx-extension.zip .` | -| Deploy Worker | `cd services/worker && npx wrangler deploy` | +| Install | `npm install` | +| Build everything | `npm run build` (the site build needs the extension build, which runs first) | +| Test, lint, type check | `npm test`, `npm run lint`, `npx tsc --noEmit` in a package | +| Load the extension | `chrome://extensions`, Developer mode, Load unpacked: `apps/extension/dist/` | +| Preview the site | `npx wrangler pages dev apps/site/dist` | +| Package for the stores | `npm run package`: `release/meterx--chromium.zip` (Chrome, Edge, Opera), `-firefox.zip`, `-source.zip` (upload with the Firefox build) | +| Deploy the worker | `cd services/worker && npx wrangler deploy` | +| Deploy the site | `npm run deploy -w @meterx/site` (Cloudflare Pages project `meterx-site`, custom domain `meterx.ajithakdev.com`) | +| Bump the version | Edit `version` in the root `package.json` only; the build stamps it everywhere | +| Release | Move CHANGELOG Unreleased under the version, commit `chore: release vX.Y.Z`, tag, push the tag, `gh release create`, then deploy the worker and site and upload the store zips | +| Regenerate brand images | `npm run build`, then `node brand/build.mjs` (see the header of that file for the env vars) | +| Regenerate the code map | `node scripts/codemap.mjs` | ## When something goes wrong | Problem | Fix | | --- | --- | -| Express rate limit error behind reverse proxy | Set `app.set('trust proxy', 1)` in Express to read client IPs through proxy headers | -| Custom server URL rejected | Use https for remote servers. Plain http is only allowed for localhost, 127.0.0.1, or .local domains | -| Bufferbloat inflates ping after upload | A buffer drain delay separates the upload test and ping test in the background worker | -| Blank PDF report export | Render the container offscreen with visible styling instead of using opacity zero | -| Server returns 429 Too Many Requests | Wait one minute for the rate limit window to reset | +| Site quick test or settings check says it cannot reach the server | The worker must allow the site origin: deploy it with `wrangler deploy` | +| Custom server URL rejected | Use https for remote servers. Plain http is only allowed for localhost, 127.0.0.1 and .local | +| Server returns 429 Too Many Requests | Wait a minute for the rate limit window to reset | +| Firefox upload rejected for a missing id | Upload the `-firefox.zip` from `npm run package`, not the Chromium zip | +| Site build fails with "Build the extension first" | Run `npm run build` from the root, or build `@meterx/extension` before `@meterx/site` | +| The demo pages on the site look out of date | Rebuild: the site copies the current extension build into `/app/` | ## More detail -- docs/STATE.md: What is in progress and what comes next. -- docs/KNOWN-ISSUES.md: Open bugs by area, each linked to its GitHub issue. -- docs/CODEMAP.md: File map with line counts and the extension's message protocol. -- README.md: Architecture overview, feature list, and server endpoint details. -- CONTRIBUTING.md: Development setup, commands, code style, and pull request guidelines. -- CHANGELOG.md: Version history and migration notes. -- privacy-policy.md: Extension and server data handling policy. -- TRADEMARK.md: Brand usage rules for the MeterX name and logo. -- LICENSE: AGPL-3.0 terms for the codebase. +- docs/STATE.md: what is in progress and what comes next +- docs/CODEMAP.md: every source file, with the message protocol +- docs/SITE-PLAN.md: the site's pages, hosting and brand assets +- docs/KNOWN-ISSUES.md: open bugs by area +- docs/SEO.md: the site's SEO setup and release checklist +- CHANGELOG.md, CONTRIBUTING.md, TRADEMARK.md, LICENSE diff --git a/apps/extension/src/lib/browser.ts b/apps/extension/src/lib/browser.ts index c51f8b2..bbee6e8 100644 --- a/apps/extension/src/lib/browser.ts +++ b/apps/extension/src/lib/browser.ts @@ -7,7 +7,8 @@ /// /** Default backend. Also in static/manifest.json host_permissions; change both together. */ -export const DEFAULT_SERVER_URL = 'https://meterx-speedtest.meterx-ajithakdev.workers.dev'; +export { WORKER_URL as DEFAULT_SERVER_URL } from '@meterx/shared'; +import { WORKER_URL as DEFAULT_SERVER_URL } from '@meterx/shared'; export const SITE_URL = 'https://meterx.ajithakdev.com'; export const REPO_URL = 'https://github.com/ajithakdev/meterx'; diff --git a/apps/extension/src/pages/options.ts b/apps/extension/src/pages/options.ts index a4f5387..eddc293 100644 --- a/apps/extension/src/pages/options.ts +++ b/apps/extension/src/pages/options.ts @@ -6,9 +6,9 @@ /// -import { VERSION, type IspPlan } from '@meterx/shared'; +import { VERSION, WORKER_URL, type IspPlan } from '@meterx/shared'; -const DEFAULT_URL = 'https://meterx-speedtest.meterx-ajithakdev.workers.dev'; +const DEFAULT_URL = WORKER_URL; const ISP_KEY = 'ispPlan'; const SCHEDULE_KEY = 'scheduleMinutes'; const NOTIFY_KEY = 'notifyOnDrop'; diff --git a/apps/site/src/quick-test.ts b/apps/site/src/quick-test.ts index c2fa282..fa3f23b 100644 --- a/apps/site/src/quick-test.ts +++ b/apps/site/src/quick-test.ts @@ -4,7 +4,7 @@ * @license AGPL-3.0 */ -const WORKER = 'https://meterx-speedtest.meterx-ajithakdev.workers.dev'; +import { WORKER_URL as WORKER } from '@meterx/shared'; async function pingMs(): Promise { const t = performance.now(); diff --git a/apps/site/static/index.html b/apps/site/static/index.html index b9c983c..efa7c1b 100644 --- a/apps/site/static/index.html +++ b/apps/site/static/index.html @@ -21,6 +21,7 @@ + diff --git a/apps/site/static/robots.txt b/apps/site/static/robots.txt index 5c82dbc..1c3fbc4 100644 --- a/apps/site/static/robots.txt +++ b/apps/site/static/robots.txt @@ -1,4 +1,6 @@ User-agent: * Allow: / Disallow: /r/ +Disallow: /app/ +Disallow: /get/ Sitemap: https://meterx.ajithakdev.com/sitemap.xml diff --git a/apps/site/static/sitemap.xml b/apps/site/static/sitemap.xml index 6dd0636..5212f33 100644 --- a/apps/site/static/sitemap.xml +++ b/apps/site/static/sitemap.xml @@ -1,6 +1,9 @@ https://meterx.ajithakdev.com/ + https://meterx.ajithakdev.com/how-it-works/ + https://meterx.ajithakdev.com/features/ + https://meterx.ajithakdev.com/faq/ https://meterx.ajithakdev.com/about/ https://meterx.ajithakdev.com/privacy/ diff --git a/docs/CODEMAP.md b/docs/CODEMAP.md index 5b2f98c..4690273 100644 --- a/docs/CODEMAP.md +++ b/docs/CODEMAP.md @@ -1,104 +1,96 @@ # MeterX code map -Mapped at commit `e5e7b9c` (2026-10-22). - -Check for drift before trusting this file: `git diff --stat e5e7b9c..HEAD -- packages` - -Numbers in parentheses are line counts. Use them to judge read cost. +The layout and the rules behind it are in CLAUDE.md ("Architecture"). This file lists every source file. ## Architecture in one screen ``` -popup.html / sidepanel.html / options.html (extension pages) - popup.ts, sidepanel.ts, options.ts - chrome.runtime.sendMessage({action}) ──► background.ts (service worker) - ◄── testProgress / testComplete / testError │ - │ fetch - ┌──────────────────────────────────┴───────────┐ - ▼ ▼ - services/worker (Cloudflare Worker, default) services/server (Express, self-host) - /ping /test-file/:n MB.bin /upload /health /ip same routes, rate limited +apps/extension pages (popup, side panel, settings) --chrome.runtime messages--> service worker (background/) + | fetch +apps/site static pages + the extension pages in demo mode (/app/) v +services/worker (Cloudflare, default) services/server (Express, self-host) same API: /ping /test-file /upload /health /ip +packages/shared contract, version, quality rules, formatting, share links (pure) +packages/reports PDF, CSV, JSON builders and stats (pure) ``` -- All measurement runs in the service worker. The pages only send messages and render. -- Default backend: `https://meterx-speedtest.meterx-ajithakdev.workers.dev`. It appears in `background.ts` (`DEFAULT_SERVER_URL`), `options.ts` (`DEFAULT_URL`) and `static/manifest.json` (`host_permissions`). -- `packages/shared` is types plus two constant tables, imported as raw TS and bundled into each package. -- Settings live in `chrome.storage.sync`: `serverUrl`, `ispPlan`, `scheduleMinutes`, `notifyOnDrop`. History lives in `chrome.storage.local` under `history`. +- Measurement runs in the service worker; the pages only send messages and render. +- Default backend: `WORKER_URL` in `packages/shared`, also in the extension manifest's `host_permissions` and the site's `_headers` CSP. +- Settings live in `chrome.storage.sync` (`serverUrl`, `ispPlan`, `scheduleMinutes`, `notifyOnDrop`); history in `chrome.storage.local` under `history`. ## Message protocol | Message | Sent by | Handled in | Notes | |---|---|---|---| -| `startTest` | popup, side panel | `background.ts:539` | Runs `runFullTest`, replies with final status. | -| `cancelTest` | popup | `background.ts:547` | Aborts `testAbortController`. | -| `getServerUrl` / `setServerUrl` | (unused by pages today) | `background.ts:554`, `:570` | Options page writes storage directly. | -| `rescheduleTests` | options | `background.ts:558` | Re-reads `scheduleMinutes`. | -| `openSidePanel` | nothing | `background.ts:562` | Unreachable, see #72. | +| `startTest` | popup, side panel | `background/index.ts` | Runs `runFullTest`, replies with final status. | +| `cancelTest` | popup | `background/index.ts` | Aborts `testAbortController`. | +| `getServerUrl` / `setServerUrl` | (unused by pages today) | `background/index.ts` | Options page writes storage directly. | +| `rescheduleTests` | options | `background/index.ts` | Re-reads `scheduleMinutes`. | +| `openSidePanel` | nothing | `background/index.ts` | Unreachable, see #72. | | `testProgress` / `testComplete` / `testError` | background | popup, side panel | Broadcast with `chrome.runtime.sendMessage`. | -## Files - -### Root - -| File | Notes | -|---|---| -| `package.json` | npm workspaces `packages/*`. Scripts: `build`, `test`, `lint`, `start` (Express from `services/server/dist`). | -| `eslint.config.js` | Flat config for extension TS, build scripts, server TS, CSS and Markdown. Worker and shared are not covered (#86). | -| `.github/workflows/ci.yml` | One `check` job on Node 22: lint, `tsc` for server and extension, build, test, then zips `apps/extension/dist` on pushes to `main`. | -| `.github/dependabot.yml` | Grouped monthly updates. | -| `RESOURCES-AND-GUIDE.md` | Links, accounts, how-tos. | -| `CHANGELOG.md`, `README.md`, `CONTRIBUTING.md`, `privacy-policy.md`, `TRADEMARK.md` | Human docs. Several are out of date (#90, #91, #92). | -| `.junie/guidelines.md` | Stale, describes the pre-monorepo layout (#93). | - -### apps/extension -| File | Exports / role | Notes | -|---|---|---| -| `build.mjs` (28) | esbuild script | Bundles `background`, `popup`, `options`, `sidepanel` as ESM for `chrome110`, then copies `static/` into `dist/`. | -| `src/background.ts` (581) | service worker | Constants at the top (streams, durations, trims). `measureDownloadSpeed` (196), `measureUploadSpeed` (268), `measurePingAndJitter` (341), `runFullTest` (400), `startLoadedPingProbe` (103) for bufferbloat, `extractDnsMs` (153), alarms and notifications (482-527), message router (537). | -| `src/popup.ts` (402) | popup page | Hero gauge, step progress, stat bars, history list, export menu, share button. Saves history on `testComplete` (#61). | -| `src/options.ts` (261) | options page | Server preset vs custom URL, host-permission request, connection test via `/health`, ISP plan, schedule and notify settings. | -| `src/sidepanel.ts` (78) | side panel page | Last result, chart, history, ISP %, Start button. | -| `src/history.ts` (49) | `MAX_HISTORY` (200), `HISTORY_DISPLAY` (7), `saveHistory`, `loadHistory`, `loadAllHistory`, `renderHistory` | `chrome.storage.local` key `history`, newest first. | -| `src/quality.ts` (49) | `getQuality`, `getQualityClass`, `scoreUseCases`, `QualityLabel` | Has tests. `background.ts` keeps its own copy of the grading (#67). | -| `src/isp.ts` (64) | `loadIspPlan`, `saveIspPlan`, `renderIspComparison` | `chrome.storage.sync` key `ispPlan`. | -| `src/share.ts` (61) | `buildShareUrl`, `parseShareUrl` | Encodes a result into the URL fragment of `https://meterx.dev/share`, which does not exist yet (#74). Has tests. | -| `src/lib/export-pdf.ts` | `exportPdfReport`, `buildReport` | Vector PDF drawn with jsPDF in the app's light design: summary, load, readiness, plan, trend chart, paginated results. | -| `src/lib/export-data.ts` | `exportJson`, `exportCsv`, `COLUMNS` | One column list (units in the names) feeds CSV and JSON; JSON adds a versioned `format` and a summary. | -| `src/lib/report-stats.ts` | `summarize`, `consistency`, `trendPercent` | Pure stats shared by the PDF and JSON. | -| `src/chart.ts` (72), `src/sparkline.ts` (59) | `renderChart`, `renderSparkline` | Inline SVG strings. | -| `src/tooltips.ts` (67) | `TOOLTIP_COPY`, `bindTooltips` | Binds `[data-tip]` elements. | -| `src/format.ts` (19) | `fmt`, `getTimeAgo` | Has tests. | -| `static/manifest.json` | MV3 manifest | Permissions: storage, alarms, notifications, sidePanel. Custom servers use `optional_host_permissions`. Version is its own Chrome-style string. | -| `static/*.html`, `static/*.css` | Pages and styles | `popup.css` (556) is the largest. | -| `vitest.config.mjs` | Tests in `src/**/*.test.ts` | | - -### services/worker - -| File | Notes | -|---|---| -| `src/index.ts` (220) | Default-export `fetch`. `corsHeaders` (19), `getEdgeLocation` from `cf.colo` (41), `streamBinaryData` 64 KB chunks (50), handlers for download (72), upload (91, drains body), ping (119), ip (137), health (158). No rate limit (#79). | -| `wrangler.toml` | Name `meterx-speedtest`, observability on. | - -### services/server - -| File | Notes | -|---|---| -| `src/app.ts` (99) | Express 5 app: CORS (extension origins + `CORS_ORIGINS`), per-route rate limits, zero-filled test files in `test-files/` created on demand, `/ping`, `/test-file/:filename`, `/upload`, `/health`, `/ip`. | -| `src/server.ts` (7) | `app.listen(PORT || 3000)`. Built to `dist/server.js` by esbuild. | -| `src/app.test.ts` (76) | Vitest + supertest, 10 tests. | -| `src/test.ts` (88) | Legacy CommonJS smoke script, not run, broken (#93). | - -### packages/shared +## Files -| File | Notes | -|---|---| -| `src/index.ts` (84) | `Quality`, `TestProgress`, `TestResults`, `PingResult`, `HealthResponse`, `HistoryEntry`, `IspPlan`, `QUALITY_THRESHOLDS`, `USE_CASE_REQUIREMENTS`. `HealthResponse` and `QUALITY_THRESHOLDS` are not used anywhere (#84, #67). | +Generated by `node scripts/codemap.mjs`; regenerate after moving or adding files. Numbers are line counts. + +| File | Exports | What it does | +| --- | --- | --- | +| `apps/extension/build.mjs` (41) | - | | +| `apps/extension/src/background/index.ts` (120) | - | MeterX service worker: runs tests, saves history, schedules, notifies. | +| `apps/extension/src/background/measure.ts` (77) | `ByteSample`, `toMbps`, `bytesAtTime`, `steadyMbps`, `latencyStats`, `uploadChunkBytes` | Pure measurement math. No I/O, so it is unit tested (measure.test.ts). | +| `apps/extension/src/background/runner.ts` (227) | `Reporter`, `CancelledError`, `HttpError`, `runTest` | One speed test: connect, idle latency, download, upload. | +| `apps/extension/src/lib/browser.ts` (57) | `SITE_URL`, `REPO_URL`, `validateServerUrl`, `normalizeServerUrl`, `getServerUrl`, `panelSupported` | Settings and cross-browser helpers used by the service worker and the pages. | +| `apps/extension/src/lib/export-data.ts` (28) | `triggerDownload`, `exportCsv`, `exportJson` | CSV and JSON downloads. The files themselves are built by @meterx/reports. | +| `apps/extension/src/lib/export-pdf.ts` (16) | `exportPdfReport` | PDF download: reads the saved plan, builds the report with @meterx/reports, saves it. | +| `apps/extension/src/lib/format.ts` (7) | - | Formatting lives in @meterx/shared so the reports and the site use the same rules. | +| `apps/extension/src/lib/history.ts` (29) | `MAX_HISTORY`, `loadHistory`, `saveHistory`, `clearHistory` | Test history in chrome.storage.local (newest first). Written only by the service worker. | +| `apps/extension/src/lib/isp.ts` (25) | `loadIspPlan`, `saveIspPlan` | The user's advertised ISP plan, in chrome.storage.sync. | +| `apps/extension/src/lib/quality.ts` (17) | `QUALITY_COLORS` | Quality rules live in @meterx/shared so the site uses the same ones; this file adds the toolbar badge colours. | +| `apps/extension/src/lib/tooltips.ts` (45) | `TOOLTIP_COPY`, `bindTooltips` | Plain-language explanations for metric names, from [data-tip] attributes. | +| `apps/extension/src/lib/ui.ts` (133) | `animatePhase`, `MARK_SVG`, `edgeName`, `scopeLine`, `setFigure`, `useCasesOf` | Rendering shared by the popup and the side panel. DOM only, no storage. | +| `apps/extension/src/pages/options.ts` (265) | - | MeterX Options Page | +| `apps/extension/src/pages/popup.ts` (171) | - | Popup: run a test, see the latest result. | +| `apps/extension/src/pages/sidepanel.ts` (244) | - | Side panel: history, trend and the plan comparison. Also opens as a tab | +| `apps/site/build.mjs` (41) | - | | +| `apps/site/src/capacity.ts` (30) | `Capacity`, `capacity`, `formatDuration` | What a download speed buys, using the extension's own thresholds: 25 Mbps for a 4K stream, 5 Mbps for HD. | +| `apps/site/src/common.ts` (48) | `wireInstall` | Every page: install buttons for the visitor's browser, and section entrances. | +| `apps/site/src/demo-shim.ts` (99) | - | Runs the real extension pages (popup, side panel, settings) on the website in demo mode. | +| `apps/site/src/get.ts` (12) | - | /get: one link to share anywhere; sends each visitor to their own browser's store. | +| `apps/site/src/home.ts` (115) | - | Home page: scroll drives the real popup through a sample test, the phase log follows it, | +| `apps/site/src/quick-test.ts` (49) | `quickTest` | A short in-page check against the MeterX worker: a few pings and one download stream for about five seconds. | +| `apps/site/src/sample.ts` (62) | `SAMPLE_PLAN`, `sampleHistory`, `sampleResult`, `PHASES`, `sampleFrame` | Sample data for the demo pages: a week of results on a 300/100 plan and one test played frame by frame. | +| `apps/site/src/share.ts` (38) | - | Share viewer: the result lives in the link's hash, so it never reaches a server. | +| `apps/site/src/stores.ts` (23) | `StoreId`, `STORES`, `detectStore` | Where MeterX is listed. An empty url means the listing is not live yet; buttons then point at the install list. | +| `brand/build.mjs` (161) | - | Builds every MeterX brand asset from the mark defined below: SVG masters, extension and site icons, | +| `packages/reports/src/data.ts` (78) | `COLUMNS`, `toCsv`, `toJson`, `escapeCsv` | CSV and JSON exports. One column list feeds both, so they always carry the same fields. | +| `packages/reports/src/index.ts` (10) | - | Reports built from MeterX results: the PDF, CSV and JSON exports and their summary stats. | +| `packages/reports/src/pdf.ts` (348) | `buildReport`, `reportId`, `niceMax` | PDF report, drawn as vector text and shapes with jsPDF (loaded on demand). | +| `packages/reports/src/stats.ts` (80) | `consistency`, `trendPercent`, `Spread`, `Summary`, `summarize` | Pure stats for the exports (PDF report and JSON summary), split out so the edge cases are tested. | +| `packages/shared/src/format.ts` (44) | `NONE`, `fmt`, `fmtSpeed`, `localDate`, `getTimeAgo`, `fmtWhen` | Formatting helpers. | +| `packages/shared/src/index.ts` (112) | `VERSION`, `ALLOWED_SIZES_MB`, `TEST_FILE_RE`, `MAX_UPLOAD_BYTES`, `WORKER_URL`, `SITE_ORIGIN` | MeterX shared contract: types, API constants and helpers used by the | +| `packages/shared/src/quality.ts` (60) | `QualityLabel`, `getQuality`, `qualityOf`, `UseCaseScore`, `UseCaseScores`, `scoreUseCases` | Quality grade and use-case readiness. The one copy of these rules (#67). | +| `packages/shared/src/share.ts` (66) | `SHARE_VIEWER`, `buildShareUrl`, `parseShareUrl` | Share links: the result is base64url JSON in the URL fragment, so it never | +| `packages/shared/src/stats.ts` (28) | `median`, `gradeBufferbloat`, `planShare` | Pure measurement helpers shared by the extension, the reports and the site. | +| `scripts/codemap.mjs` (41) | - | Regenerates the file table in docs/CODEMAP.md: every source file with its line count, exports and doc line. | +| `scripts/package-stores.mjs` (50) | `toFirefox` | Builds the extension and writes one upload per store into release/: | +| `services/server/src/app.ts` (82) | - | MeterX self-host speed test server (same API contract as the Worker). | +| `services/server/src/server.ts` (8) | - | | +| `services/worker/src/index.ts` (121) | `Env` | MeterX Cloudflare Worker: edge speed test server. | ## Tests -| Package | Files | Run one | -|---|---|---| -| extension | `format`, `quality`, `share` (21 tests) | `cd apps/extension && npx vitest run src/quality.test.ts` | -| server | `app.test.ts` (10 tests) | `cd services/server && npx vitest run src/app.test.ts` | -| worker, shared | none | (#86) | +- `apps/extension/src/background/measure.test.ts` +- `apps/extension/src/lib/browser.test.ts` +- `apps/extension/src/lib/quality.test.ts` +- `apps/site/src/capacity.test.ts` +- `apps/site/src/sample.test.ts` +- `apps/site/src/stores.test.ts` +- `packages/reports/src/data.test.ts` +- `packages/reports/src/pdf.test.ts` +- `packages/reports/src/stats.test.ts` +- `packages/shared/src/format.test.ts` +- `packages/shared/src/origin.test.ts` +- `packages/shared/src/share.test.ts` +- `scripts/package-stores.test.mjs` +- `services/server/src/app.test.ts` +- `services/worker/src/index.test.ts` diff --git a/docs/SEO.md b/docs/SEO.md new file mode 100644 index 0000000..75b2750 --- /dev/null +++ b/docs/SEO.md @@ -0,0 +1,49 @@ +# SEO guide + +How search and link previews work on `meterx.ajithakdev.com`, what to set up once, and what to check on every release. + +## How the site handles SEO + +- Every page is plain static HTML in `apps/site/static/`, so crawlers get the full text, title and meta tags without running JavaScript. +- Each page has its own ``, `<meta name="description">`, `<link rel="canonical">` and Open Graph tags in its `<head>`. The home page also carries `SoftwareApplication` JSON-LD. +- `apps/site/static/sitemap.xml` lists the six indexable pages. Update it by hand when a page is added or removed. +- `apps/site/static/robots.txt` allows everything except `/r/` (shared results), `/app/` (the extension's demo pages) and `/get/` (a redirect). +- The share viewer and 404 pages carry `noindex`. +- `apps/site/static/img/og.png` (1200x630) is the preview image for shared links. `brand/build.mjs` makes it. +- The site is served by Cloudflare Pages on the `ajithakdev.com` zone. Cloudflare Crawler Hints, if turned on for the zone, notify IndexNow on changes. + +## One-time setup + +Do these once. Tick them off here when done. + +- [ ] Google Search Console: the `ajithakdev.com` Domain property (set up for the portfolio) already covers `meterx.ajithakdev.com`. Submit `https://meterx.ajithakdev.com/sitemap.xml` under Sitemaps. +- [ ] Bing Webmaster Tools: add `https://meterx.ajithakdev.com` (or import from Search Console) and submit the same sitemap. +- [ ] In Search Console, URL Inspection on `https://meterx.ajithakdev.com/`, then "Request indexing". +- [ ] Link the site from each store listing's website field (Firefox, Edge, Opera). + +## Release checklist + +A minor release (copy edits, small fixes) only needs the first section. A major release (new page, removed page, renamed URL) needs all of it. + +### Every release + +- [ ] `npm run build` passes. +- [ ] After deploy, each page returns its own title: + ```sh + for p in "" how-it-works/ features/ faq/ about/ privacy/; do curl -s https://meterx.ajithakdev.com/$p | grep -o '<title>[^<]*'; done + ``` +- [ ] A changed title stays under about 60 characters, a description under about 160. + +### Major release + +- [ ] New page: add it to `sitemap.xml`, give it a unique title, description and canonical URL, and add it to the header and footer partials in `apps/site/static/_partials/`. +- [ ] Removed or renamed page: add a 301 in `apps/site/static/_redirects` (create it if missing). Never let an old URL return a 404 if anyone might have linked to it. +- [ ] Link preview looks right: paste the URL into https://www.linkedin.com/post-inspector/. +- [ ] Lighthouse SEO score in Chrome DevTools is 100 on the changed pages, on mobile. +- [ ] Resubmit the sitemap in Search Console and request indexing on new or changed pages. + +## Monthly check + +- Search Console Pages report: anything "not indexed" that should be, and why. +- Core Web Vitals: anything poor on mobile. The home page loads the popup in a frame; watch its LCP. +- Performance: which searches bring people in, and whether a title or description should say it more clearly. diff --git a/docs/SITE-PLAN.md b/docs/SITE-PLAN.md index e227cf7..2d02107 100644 --- a/docs/SITE-PLAN.md +++ b/docs/SITE-PLAN.md @@ -1,6 +1,6 @@ # MeterX site plan -Status: plan, not built. Written 2026-10-24 for the 2027.0.0 release. +Status: built on 2026-10-24 (multi-page, with the extension pages in demo mode). Not deployed yet. The plan below is kept for the reasoning; CLAUDE.md describes what exists. ## Goal diff --git a/docs/STATE.md b/docs/STATE.md index db88613..29a6f0e 100644 --- a/docs/STATE.md +++ b/docs/STATE.md @@ -4,29 +4,23 @@ Last updated: 2026-10-24, on `release/v2027.0.0` at `b4e5b7c` (not pushed, no PR Update this file at the end of each work session. Keep it short. Delete entries once they are done and merged. -## In progress (handoff, 2026-10-24 night) +## In progress (2026-10-24) -Major release 2027.0.0 on branch `release/v2027.0.0`, planned for Sunday. Not pushed, no PR yet. The owner tests locally before the PR is opened, and merges only on their word. +Major release 2027.0.0 on branch `release/v2027.0.0`, for Sunday. Not pushed, no PR yet. The owner tests locally before the PR is opened and merges only on their word. Done on the branch: -- Re-architecture, full redesign (popup, side panel, settings), live test view in the side panel, side panel fits the window (results list scrolls under pinned headers). -- Exports redesigned: vector PDF (watermark, Report ID), CSV and JSON with units and a summary. -- One version source (root `package.json`, stamped into the manifest at build). No em or en dashes anywhere. -- Logo chosen: option 8, "X on a dial". `brand/build.mjs` makes every asset (icons, maskable, Edge 300 logo, 440x280 tile, 1400x560 marquee, OG and GitHub cards, five 1280x800 store screenshots) and copies icons into the extension and site. Alternates kept in `brand/alternates/` (rising bars, up/down X). -- Site `apps/site` (plan in `docs/SITE-PLAN.md`): home with live quick test, "What can X Mbps do", features with real screenshots, pinned test phases, install, FAQ; plus `/about`, `/privacy` (rewritten for #92), `/r/` share viewer (#74), `/get`, 404. Taste-skill inspector: 0 FAIL at 1440, 768, 390 (warnings: font-size count, no video). -- Worker CORS allows the site origin; quality rules moved to `@meterx/shared`. - -Home page redesigned 2026-10-24 after owner feedback: centred 1120px column, hero with headline, two actions and the real popup screenshot, quick test as a secondary button with its result on a reserved line (no layout shift, checked at 1440 and 390), full-width feature rows, one proportional phase bar with four steps. Waiting for the owner's review. +- Repo renamed to `ajithakdev/meterx` and split into `apps/` (extension, site), `services/` (worker, server) and `packages/` (shared, reports). `npm run package` makes the store zips. +- Extension: redesign, live test view, exports (vector PDF with report ID, CSV, JSON), one version source, new X-on-a-dial logo. +- Site (`apps/site`): Home (scroll drives the real popup through a sample test), How it works, Features, Questions, About, Privacy, share viewer, `/get`. The extension's own pages run on the site in demo mode through `src/demo-shim.ts`. Shared header and footer partials. +- Brand: `brand/build.mjs` makes icons, store tiles, marquee, social cards and store screenshots. +- Docs: CLAUDE.md, CODEMAP (generated by `scripts/codemap.mjs`), README, RESOURCES-AND-GUIDE, SEO.md. Next, in order: -1. Owner checks the site locally: `cd apps/site && node build.mjs && npx wrangler pages dev dist` (or any static server on `dist`). -2. Fill the Edge and Opera listing URLs in `apps/site/src/stores.ts` (and the names on the home page). -3. Look at the phone renders of the site once more (`.taste-skill/inspect/mobile-*.jpg`, rerun the inspector from `apps/site/dist`); add a motion pass if needed. -4. Cloudflare Pages: create project `meterx-site` (free), `npm run deploy -w @meterx/site`, add custom domain `meterx.ajithakdev.com`. Write `docs/SEO.md`, add site rows to `RESOURCES-AND-GUIDE.md`. -5. Verify the scan's bug fixes against their issues (#60, #64 to #70, #73, #76 to #79, #81, #82, #85) and close them; check #61, #62, #63, #80, #87. -6. Firefox build check, README refresh (it is stale), open the PR, then on Sunday: merge on the owner's word, tag `v2027.0.0`, `wrangler deploy` the worker, upload to the stores with the `brand/png/store/` images, deploy the site, add MeterX to the portfolio. - -Brand regeneration: `PLAYWRIGHT= CHROMIUM= REPORT_PNG= node brand/build.mjs` after `npm run build`. +1. Owner reviews the site locally: `npm run build && npx wrangler pages dev apps/site/dist`. +2. Opera listing link (search found none; it may still be in review) into `apps/site/src/stores.ts`. +3. Cloudflare Pages project `meterx-site` (free) and the `meterx.ajithakdev.com` domain; `npm run deploy -w @meterx/site`. +4. Verify the scan's fixes against their issues and close them (#60, #64 to #70, #73, #76 to #79, #81, #82, #85); check #61 to #63, #80, #87. Open the PR. +5. Sunday: merge on the owner's word, tag `v2027.0.0`, `wrangler deploy` the worker (needed for the site's quick test), upload `release/` zips with `brand/png/store/` images, deploy the site, add MeterX to the portfolio. ## Releases diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index df013b6..2807fef 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -16,6 +16,9 @@ export const TEST_FILE_RE = /^(1|5|10|25)MB\.bin$/; /** Bodies above this are cut off by both backends; the extension never sends more than 8 MB. */ export const MAX_UPLOAD_BYTES = 16 * 1024 * 1024; +/** The default test server. Also listed in the extension's manifest host_permissions and the site's CSP; change all three together. */ +export const WORKER_URL = 'https://meterx-speedtest.meterx-ajithakdev.workers.dev'; + /** The product site, which runs a short in-page test against the worker. */ export const SITE_ORIGIN = 'https://meterx.ajithakdev.com'; diff --git a/privacy-policy.md b/privacy-policy.md index 8eaeee5..a465386 100644 --- a/privacy-policy.md +++ b/privacy-policy.md @@ -1,66 +1,5 @@ -# Privacy Policy for MeterX +# MeterX privacy policy -**Last Updated:** March 20, 2026 +The current policy is published at [meterx.ajithakdev.com/privacy](https://meterx.ajithakdev.com/privacy/). Its source is [apps/site/static/privacy/index.html](apps/site/static/privacy/index.html), and that file's history is the policy's change log. -This privacy policy explains how MeterX, created by [ajithakdev](https://github.com/ajithakdev), handles your information when you use the MeterX browser extension and its speed test infrastructure. - -## 1. What We Collect - -### Extension (runs in your browser) - -- **Speed test results** — download, upload, ping, jitter, packet loss. Displayed in the popup and stored locally in your browser via `chrome.storage.local`. This data never leaves your device unless you export a report. -- **Test history** — up to 200 past results stored locally. Visible only to you. -- **Server URL preference** — if you configure a custom server, that URL is saved in `chrome.storage.sync`. - -### Server interaction (during a speed test) - -When you run a test, the extension connects to a Cloudflare Worker (`meterx-speedtest.meterx-ajithakdev.workers.dev`) or your configured custom server. During this connection: - -- **Your IP address** is sent to the server as part of normal HTTPS communication. Cloudflare Workers do not log request IPs by default. -- **Download test** — the server streams binary data to your browser. No data about you is collected. -- **Upload test** — your browser sends binary data to the server. The server discards this data immediately after receiving it. Nothing is stored. -- **Ping test** — minimal HEAD requests for latency measurement. No payload. -- **No cookies, no tracking, no analytics.** The server is stateless. - -## 2. How We Use Your Information - -- **To run the speed test** — that's it. The server exists solely to send/receive test data. -- **To show you results** — stored locally in your browser, never transmitted elsewhere. -- **PDF reports** — generated entirely in your browser using html2canvas. No server involved. - -## 3. Data Sharing - -- **We do not sell your data.** Ever. -- **Cloudflare** — the default speed test server runs on Cloudflare Workers. Cloudflare's infrastructure processes the HTTPS requests. See [Cloudflare's privacy policy](https://www.cloudflare.com/privacypolicy/) for their data handling practices. -- **Self-hosted servers** — if you configure a custom server URL, your test data goes to that server instead. MeterX has no visibility into third-party servers. -- **Legal** — we may disclose information if required by law. - -## 4. Data Retention - -- **Local data** — stays on your device until you clear browser storage or uninstall the extension. -- **Server-side** — the Cloudflare Worker stores nothing. It processes requests and discards all data immediately. There are no databases, no logs, no files. - -## 5. Security - -All communication between the extension and the server uses HTTPS. The extension requests only the minimum permissions needed (`storage` for saving results, host permission for the speed test server). - -## 6. Your Choices - -- Run a test or don't — it's always your choice. -- Clear your history anytime from browser storage. -- Use a self-hosted server if you don't want to connect to the default Cloudflare endpoint. -- Export your data as a PDF report that you control. - -## 7. Children's Privacy - -MeterX is not designed for children under 13. We do not knowingly collect information from children. - -## 8. Changes - -If this policy changes, the "Last Updated" date at the top will reflect it. - -## 9. Contact - -- **Author:** Ajith Kumar K ([@ajithakdev](https://github.com/ajithakdev)) -- **Email:** ajithkumarks2579@gmail.com -- **GitHub:** [github.com/ajithakdev/meterx/issues](https://github.com/ajithakdev/meterx/issues) +In short: MeterX collects no personal data. Results and settings stay in your browser's extension storage, the default test server keeps no logs, and nothing is sent anywhere except the test traffic itself. diff --git a/scripts/codemap.mjs b/scripts/codemap.mjs new file mode 100644 index 0000000..51ba257 --- /dev/null +++ b/scripts/codemap.mjs @@ -0,0 +1,40 @@ +/** + * Regenerates the file table in docs/CODEMAP.md: every source file with its line count, exports and doc line. + * Run after moving or adding files: node scripts/codemap.mjs + * @license AGPL-3.0 + */ + +import { execFileSync } from 'child_process'; +import { readFileSync, writeFileSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const tracked = execFileSync('git', ['ls-files'], { cwd: root, encoding: 'utf8' }).split('\n'); +const files = tracked.filter(f => /^(apps\/\w+\/(src\/|build\.mjs)|services\/\w+\/src\/|packages\/\w+\/src\/|brand\/build\.mjs|scripts\/)/.test(f) + && /\.(ts|mjs)$/.test(f) && !/\.test\.(ts|mjs)$/.test(f)); + +const rows = files.map(f => { + const src = readFileSync(resolve(root, f), 'utf8'); + const lines = src.split('\n').length; + const exports = [...src.matchAll(/^export (?:async )?(?:function|const|class|interface|type) (\w+)/gm)].map(m => m[1]); + const doc = src.match(/^\/\*\*\s*\n\s*\*\s*(.+)/)?.[1]?.trim() ?? ''; + return `| \`${f}\` (${lines}) | ${exports.slice(0, 6).map(e => `\`${e}\``).join(', ') || '-'} | ${doc.replace(/\|/g, '/')} |`; +}); + +const tests = tracked.filter(f => /\.test\.(ts|mjs)$/.test(f)); +const head = readFileSync(resolve(root, 'docs/CODEMAP.md'), 'utf8').split('\n## Files')[0]; +writeFileSync(resolve(root, 'docs/CODEMAP.md'), `${head} +## Files + +Generated by \`node scripts/codemap.mjs\`; regenerate after moving or adding files. Numbers are line counts. + +| File | Exports | What it does | +| --- | --- | --- | +${rows.join('\n')} + +## Tests + +${tests.map(t => `- \`${t}\``).join('\n')} +`); +console.log(`CODEMAP: ${rows.length} files, ${tests.length} test files`); From 52870edbd49cda4ce72c7553eeb48abe15eb6abb Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Sat, 24 Oct 2026 15:06:50 +0530 Subject: [PATCH 21/44] docs: tag the layout code blocks --- CLAUDE.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 57360b2..5d0e95b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,7 @@ Load the extension unpacked from `apps/extension/dist/` (rebuild after every cha ## Architecture -``` +```text apps/extension Manifest V3 extension (popup, side panel, settings, service worker) apps/site meterx.ajithakdev.com: static pages plus the extension's own pages in demo mode under /app/ services/worker Cloudflare Worker, the default test server (streams bytes, stores nothing) diff --git a/README.md b/README.md index 6eaef23..5a33c26 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Website: [meterx.ajithakdev.com](https://meterx.ajithakdev.com) ## Repository layout -``` +```text apps/extension Manifest V3 extension (popup, side panel, settings, service worker) apps/site meterx.ajithakdev.com, which runs the extension's own pages in demo mode services/worker Cloudflare Worker, the default test server (stores nothing) From 27f71446119c5db2b69dc768e9f4874714e3030c Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Sat, 24 Oct 2026 15:22:12 +0530 Subject: [PATCH 22/44] feat(site): store dial on Get MeterX, answer panel, ping comparison, demo polish Get MeterX puts every store on the brand's dial and points the needle at the visitor's browser, with live and upcoming listings and their logos (Simple Icons, CC0). Questions open in a side panel with a read-more link and a help card. How it works shows each phase's share of the test above its text and compares ping under load on a grade C and a grade A line. The demo answers the server check itself, the settings frame takes its page's height, and the header uses the theme-coloured mark. --- .../middleware-insertion-facade.js | 11 ++ .../bundle-6hK2Wx/middleware-loader.entry.ts | 134 +++++++++++++ .wrangler/tmp/dev-3OApoy/pages-shim.js | 185 ++++++++++++++++++ .wrangler/tmp/dev-3OApoy/pages-shim.js.map | 8 + CHANGELOG.md | 3 + apps/site/build.mjs | 2 +- apps/site/src/common.ts | 11 ++ apps/site/src/demo-shim.ts | 22 ++- apps/site/src/faq.ts | 23 +++ apps/site/src/get.ts | 55 +++++- apps/site/src/stores.ts | 17 +- apps/site/static/_partials/footer.html | 2 +- apps/site/static/_partials/header.html | 2 +- apps/site/static/faq/index.html | 25 ++- apps/site/static/features/index.html | 4 +- apps/site/static/get/index.html | 40 ++-- apps/site/static/how-it-works/index.html | 50 +++-- apps/site/static/img/glyph-on-dark.svg | 1 + apps/site/static/img/glyph-on-light.svg | 1 + apps/site/static/img/stores/brave.svg | 1 + .../site/static/img/stores/firefoxbrowser.svg | 1 + apps/site/static/img/stores/googlechrome.svg | 1 + apps/site/static/img/stores/microsoftedge.svg | 1 + apps/site/static/img/stores/opera.svg | 1 + apps/site/static/img/stores/safari.svg | 1 + apps/site/static/img/stores/vivaldi.svg | 1 + apps/site/static/styles/get.css | 45 +++++ apps/site/static/styles/site.css | 73 +++++-- 28 files changed, 659 insertions(+), 62 deletions(-) create mode 100644 .wrangler/tmp/bundle-6hK2Wx/middleware-insertion-facade.js create mode 100644 .wrangler/tmp/bundle-6hK2Wx/middleware-loader.entry.ts create mode 100644 .wrangler/tmp/dev-3OApoy/pages-shim.js create mode 100644 .wrangler/tmp/dev-3OApoy/pages-shim.js.map create mode 100644 apps/site/src/faq.ts create mode 100644 apps/site/static/img/glyph-on-dark.svg create mode 100644 apps/site/static/img/glyph-on-light.svg create mode 100644 apps/site/static/img/stores/brave.svg create mode 100644 apps/site/static/img/stores/firefoxbrowser.svg create mode 100644 apps/site/static/img/stores/googlechrome.svg create mode 100644 apps/site/static/img/stores/microsoftedge.svg create mode 100644 apps/site/static/img/stores/opera.svg create mode 100644 apps/site/static/img/stores/safari.svg create mode 100644 apps/site/static/img/stores/vivaldi.svg create mode 100644 apps/site/static/styles/get.css diff --git a/.wrangler/tmp/bundle-6hK2Wx/middleware-insertion-facade.js b/.wrangler/tmp/bundle-6hK2Wx/middleware-insertion-facade.js new file mode 100644 index 0000000..59b55c6 --- /dev/null +++ b/.wrangler/tmp/bundle-6hK2Wx/middleware-insertion-facade.js @@ -0,0 +1,11 @@ + import worker, * as OTHER_EXPORTS from "/home/ajith/projects/meterx-server/node_modules/wrangler/templates/pages-shim.ts"; + import * as __MIDDLEWARE_0__ from "/home/ajith/projects/meterx-server/node_modules/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts"; +import * as __MIDDLEWARE_1__ from "/home/ajith/projects/meterx-server/node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts"; + + export * from "/home/ajith/projects/meterx-server/node_modules/wrangler/templates/pages-shim.ts"; + const MIDDLEWARE_TEST_INJECT = "__INJECT_FOR_TESTING_WRANGLER_MIDDLEWARE__"; + export const __INTERNAL_WRANGLER_MIDDLEWARE__ = [ + + __MIDDLEWARE_0__.default,__MIDDLEWARE_1__.default + ] + export default worker; \ No newline at end of file diff --git a/.wrangler/tmp/bundle-6hK2Wx/middleware-loader.entry.ts b/.wrangler/tmp/bundle-6hK2Wx/middleware-loader.entry.ts new file mode 100644 index 0000000..b60342f --- /dev/null +++ b/.wrangler/tmp/bundle-6hK2Wx/middleware-loader.entry.ts @@ -0,0 +1,134 @@ +// This loads all middlewares exposed on the middleware object and then starts +// the invocation chain. The big idea is that we can add these to the middleware +// export dynamically through wrangler, or we can potentially let users directly +// add them as a sort of "plugin" system. + +import ENTRY, { __INTERNAL_WRANGLER_MIDDLEWARE__ } from "/home/ajith/projects/meterx-server/.wrangler/tmp/bundle-6hK2Wx/middleware-insertion-facade.js"; +import { __facade_invoke__, __facade_register__, Dispatcher } from "/home/ajith/projects/meterx-server/node_modules/wrangler/templates/middleware/common.ts"; +import type { WorkerEntrypointConstructor } from "/home/ajith/projects/meterx-server/.wrangler/tmp/bundle-6hK2Wx/middleware-insertion-facade.js"; + +// Preserve all the exports from the worker +export * from "/home/ajith/projects/meterx-server/.wrangler/tmp/bundle-6hK2Wx/middleware-insertion-facade.js"; + +class __Facade_ScheduledController__ implements ScheduledController { + readonly #noRetry: ScheduledController["noRetry"]; + + constructor( + readonly scheduledTime: number, + readonly cron: string, + noRetry: ScheduledController["noRetry"] + ) { + this.#noRetry = noRetry; + } + + noRetry() { + if (!(this instanceof __Facade_ScheduledController__)) { + throw new TypeError("Illegal invocation"); + } + // Need to call native method immediately in case uncaught error thrown + this.#noRetry(); + } +} + +function wrapExportedHandler(worker: ExportedHandler): ExportedHandler { + // If we don't have any middleware defined, just return the handler as is + if ( + __INTERNAL_WRANGLER_MIDDLEWARE__ === undefined || + __INTERNAL_WRANGLER_MIDDLEWARE__.length === 0 + ) { + return worker; + } + // Otherwise, register all middleware once + for (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) { + __facade_register__(middleware); + } + + const fetchDispatcher: ExportedHandlerFetchHandler = function ( + request, + env, + ctx + ) { + if (worker.fetch === undefined) { + throw new Error("Handler does not export a fetch() function."); + } + return worker.fetch(request, env, ctx); + }; + + return { + ...worker, + fetch(request, env, ctx) { + const dispatcher: Dispatcher = function (type, init) { + if (type === "scheduled" && worker.scheduled !== undefined) { + const controller = new __Facade_ScheduledController__( + Date.now(), + init.cron ?? "", + () => {} + ); + return worker.scheduled(controller, env, ctx); + } + }; + return __facade_invoke__(request, env, ctx, dispatcher, fetchDispatcher); + }, + }; +} + +function wrapWorkerEntrypoint( + klass: WorkerEntrypointConstructor +): WorkerEntrypointConstructor { + // If we don't have any middleware defined, just return the handler as is + if ( + __INTERNAL_WRANGLER_MIDDLEWARE__ === undefined || + __INTERNAL_WRANGLER_MIDDLEWARE__.length === 0 + ) { + return klass; + } + // Otherwise, register all middleware once + for (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) { + __facade_register__(middleware); + } + + // `extend`ing `klass` here so other RPC methods remain callable + return class extends klass { + #fetchDispatcher: ExportedHandlerFetchHandler> = ( + request, + env, + ctx + ) => { + this.env = env; + this.ctx = ctx; + if (super.fetch === undefined) { + throw new Error("Entrypoint class does not define a fetch() function."); + } + return super.fetch(request); + }; + + #dispatcher: Dispatcher = (type, init) => { + if (type === "scheduled" && super.scheduled !== undefined) { + const controller = new __Facade_ScheduledController__( + Date.now(), + init.cron ?? "", + () => {} + ); + return super.scheduled(controller); + } + }; + + fetch(request: Request) { + return __facade_invoke__( + request, + this.env, + this.ctx, + this.#dispatcher, + this.#fetchDispatcher + ); + } + }; +} + +let WRAPPED_ENTRY: ExportedHandler | WorkerEntrypointConstructor | undefined; +if (typeof ENTRY === "object") { + WRAPPED_ENTRY = wrapExportedHandler(ENTRY); +} else if (typeof ENTRY === "function") { + WRAPPED_ENTRY = wrapWorkerEntrypoint(ENTRY); +} +export default WRAPPED_ENTRY; diff --git a/.wrangler/tmp/dev-3OApoy/pages-shim.js b/.wrangler/tmp/dev-3OApoy/pages-shim.js new file mode 100644 index 0000000..f7d7c4e --- /dev/null +++ b/.wrangler/tmp/dev-3OApoy/pages-shim.js @@ -0,0 +1,185 @@ +var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); + +// node_modules/wrangler/templates/pages-shim.ts +var pages_shim_default = { + async fetch(request, env, context) { + const response = await env.ASSETS.fetch(request.url, request); + return new Response(response.body, response); + } +}; + +// node_modules/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts +var drainBody = /* @__PURE__ */ __name(async (request, env, _ctx, middlewareCtx) => { + try { + return await middlewareCtx.next(request, env); + } finally { + try { + if (request.body !== null && !request.bodyUsed) { + const reader = request.body.getReader(); + while (!(await reader.read()).done) { + } + } + } catch (e) { + console.error("Failed to drain the unused request body.", e); + } + } +}, "drainBody"); +var middleware_ensure_req_body_drained_default = drainBody; + +// node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts +function reduceError(e) { + return { + name: e?.name, + message: e?.message ?? String(e), + stack: e?.stack, + cause: e?.cause === void 0 ? void 0 : reduceError(e.cause) + }; +} +__name(reduceError, "reduceError"); +var jsonError = /* @__PURE__ */ __name(async (request, env, _ctx, middlewareCtx) => { + try { + return await middlewareCtx.next(request, env); + } catch (e) { + const error = reduceError(e); + return Response.json(error, { + status: 500, + headers: { "MF-Experimental-Error-Stack": "true" } + }); + } +}, "jsonError"); +var middleware_miniflare3_json_error_default = jsonError; + +// .wrangler/tmp/bundle-6hK2Wx/middleware-insertion-facade.js +var __INTERNAL_WRANGLER_MIDDLEWARE__ = [ + middleware_ensure_req_body_drained_default, + middleware_miniflare3_json_error_default +]; +var middleware_insertion_facade_default = pages_shim_default; + +// node_modules/wrangler/templates/middleware/common.ts +var __facade_middleware__ = []; +function __facade_register__(...args) { + __facade_middleware__.push(...args.flat()); +} +__name(__facade_register__, "__facade_register__"); +function __facade_invokeChain__(request, env, ctx, dispatch, middlewareChain) { + const [head, ...tail] = middlewareChain; + const middlewareCtx = { + dispatch, + next(newRequest, newEnv) { + return __facade_invokeChain__(newRequest, newEnv, ctx, dispatch, tail); + } + }; + return head(request, env, ctx, middlewareCtx); +} +__name(__facade_invokeChain__, "__facade_invokeChain__"); +function __facade_invoke__(request, env, ctx, dispatch, finalMiddleware) { + return __facade_invokeChain__(request, env, ctx, dispatch, [ + ...__facade_middleware__, + finalMiddleware + ]); +} +__name(__facade_invoke__, "__facade_invoke__"); + +// .wrangler/tmp/bundle-6hK2Wx/middleware-loader.entry.ts +var __Facade_ScheduledController__ = class ___Facade_ScheduledController__ { + constructor(scheduledTime, cron, noRetry) { + this.scheduledTime = scheduledTime; + this.cron = cron; + this.#noRetry = noRetry; + } + static { + __name(this, "__Facade_ScheduledController__"); + } + #noRetry; + noRetry() { + if (!(this instanceof ___Facade_ScheduledController__)) { + throw new TypeError("Illegal invocation"); + } + this.#noRetry(); + } +}; +function wrapExportedHandler(worker) { + if (__INTERNAL_WRANGLER_MIDDLEWARE__ === void 0 || __INTERNAL_WRANGLER_MIDDLEWARE__.length === 0) { + return worker; + } + for (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) { + __facade_register__(middleware); + } + const fetchDispatcher = /* @__PURE__ */ __name(function(request, env, ctx) { + if (worker.fetch === void 0) { + throw new Error("Handler does not export a fetch() function."); + } + return worker.fetch(request, env, ctx); + }, "fetchDispatcher"); + return { + ...worker, + fetch(request, env, ctx) { + const dispatcher = /* @__PURE__ */ __name(function(type, init) { + if (type === "scheduled" && worker.scheduled !== void 0) { + const controller = new __Facade_ScheduledController__( + Date.now(), + init.cron ?? "", + () => { + } + ); + return worker.scheduled(controller, env, ctx); + } + }, "dispatcher"); + return __facade_invoke__(request, env, ctx, dispatcher, fetchDispatcher); + } + }; +} +__name(wrapExportedHandler, "wrapExportedHandler"); +function wrapWorkerEntrypoint(klass) { + if (__INTERNAL_WRANGLER_MIDDLEWARE__ === void 0 || __INTERNAL_WRANGLER_MIDDLEWARE__.length === 0) { + return klass; + } + for (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) { + __facade_register__(middleware); + } + return class extends klass { + #fetchDispatcher = /* @__PURE__ */ __name((request, env, ctx) => { + this.env = env; + this.ctx = ctx; + if (super.fetch === void 0) { + throw new Error("Entrypoint class does not define a fetch() function."); + } + return super.fetch(request); + }, "#fetchDispatcher"); + #dispatcher = /* @__PURE__ */ __name((type, init) => { + if (type === "scheduled" && super.scheduled !== void 0) { + const controller = new __Facade_ScheduledController__( + Date.now(), + init.cron ?? "", + () => { + } + ); + return super.scheduled(controller); + } + }, "#dispatcher"); + fetch(request) { + return __facade_invoke__( + request, + this.env, + this.ctx, + this.#dispatcher, + this.#fetchDispatcher + ); + } + }; +} +__name(wrapWorkerEntrypoint, "wrapWorkerEntrypoint"); +var WRAPPED_ENTRY; +if (typeof middleware_insertion_facade_default === "object") { + WRAPPED_ENTRY = wrapExportedHandler(middleware_insertion_facade_default); +} else if (typeof middleware_insertion_facade_default === "function") { + WRAPPED_ENTRY = wrapWorkerEntrypoint(middleware_insertion_facade_default); +} +var middleware_loader_entry_default = WRAPPED_ENTRY; +export { + __INTERNAL_WRANGLER_MIDDLEWARE__, + middleware_loader_entry_default as default +}; +//# sourceMappingURL=pages-shim.js.map diff --git a/.wrangler/tmp/dev-3OApoy/pages-shim.js.map b/.wrangler/tmp/dev-3OApoy/pages-shim.js.map new file mode 100644 index 0000000..31f339a --- /dev/null +++ b/.wrangler/tmp/dev-3OApoy/pages-shim.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sources": ["../../../node_modules/wrangler/templates/pages-shim.ts", "../../../node_modules/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts", "../../../node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts", "../bundle-6hK2Wx/middleware-insertion-facade.js", "../../../node_modules/wrangler/templates/middleware/common.ts", "../bundle-6hK2Wx/middleware-loader.entry.ts"], + "sourceRoot": "/home/ajith/projects/meterx-server/.wrangler/tmp/dev-3OApoy", + "sourcesContent": ["// This Worker is used as a default when no Pages Functions are present.\n// It proxies the request directly on to the asset server binding.\n\nexport default >{\n\tasync fetch(request, env, context) {\n\t\tconst response = await env.ASSETS.fetch(request.url, request);\n\t\treturn new Response(response.body, response);\n\t},\n};\n", "import type { Middleware } from \"./common\";\n\nconst drainBody: Middleware = async (request, env, _ctx, middlewareCtx) => {\n\ttry {\n\t\treturn await middlewareCtx.next(request, env);\n\t} finally {\n\t\ttry {\n\t\t\tif (request.body !== null && !request.bodyUsed) {\n\t\t\t\tconst reader = request.body.getReader();\n\t\t\t\twhile (!(await reader.read()).done) {}\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tconsole.error(\"Failed to drain the unused request body.\", e);\n\t\t}\n\t}\n};\n\nexport default drainBody;\n", "import type { Middleware } from \"./common\";\n\ninterface JsonError {\n\tmessage?: string;\n\tname?: string;\n\tstack?: string;\n\tcause?: JsonError;\n}\n\nfunction reduceError(e: any): JsonError {\n\treturn {\n\t\tname: e?.name,\n\t\tmessage: e?.message ?? String(e),\n\t\tstack: e?.stack,\n\t\tcause: e?.cause === undefined ? undefined : reduceError(e.cause),\n\t};\n}\n\n// See comment in `bundle.ts` for details on why this is needed\nconst jsonError: Middleware = async (request, env, _ctx, middlewareCtx) => {\n\ttry {\n\t\treturn await middlewareCtx.next(request, env);\n\t} catch (e: any) {\n\t\tconst error = reduceError(e);\n\t\treturn Response.json(error, {\n\t\t\tstatus: 500,\n\t\t\theaders: { \"MF-Experimental-Error-Stack\": \"true\" },\n\t\t});\n\t}\n};\n\nexport default jsonError;\n", "\t\t\t\timport worker, * as OTHER_EXPORTS from \"/home/ajith/projects/meterx-server/node_modules/wrangler/templates/pages-shim.ts\";\n\t\t\t\timport * as __MIDDLEWARE_0__ from \"/home/ajith/projects/meterx-server/node_modules/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts\";\nimport * as __MIDDLEWARE_1__ from \"/home/ajith/projects/meterx-server/node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts\";\n\n\t\t\t\texport * from \"/home/ajith/projects/meterx-server/node_modules/wrangler/templates/pages-shim.ts\";\n\t\t\t\tconst MIDDLEWARE_TEST_INJECT = \"__INJECT_FOR_TESTING_WRANGLER_MIDDLEWARE__\";\n\t\t\t\texport const __INTERNAL_WRANGLER_MIDDLEWARE__ = [\n\t\t\t\t\t\n\t\t\t\t\t__MIDDLEWARE_0__.default,__MIDDLEWARE_1__.default\n\t\t\t\t]\n\t\t\t\texport default worker;", "export type Awaitable = T | Promise;\n// TODO: allow dispatching more events?\nexport type Dispatcher = (\n\ttype: \"scheduled\",\n\tinit: { cron?: string }\n) => Awaitable;\n\nexport type IncomingRequest = Request<\n\tunknown,\n\tIncomingRequestCfProperties\n>;\n\nexport interface MiddlewareContext {\n\tdispatch: Dispatcher;\n\tnext(request: IncomingRequest, env: any): Awaitable;\n}\n\nexport type Middleware = (\n\trequest: IncomingRequest,\n\tenv: any,\n\tctx: ExecutionContext,\n\tmiddlewareCtx: MiddlewareContext\n) => Awaitable;\n\nconst __facade_middleware__: Middleware[] = [];\n\n// The register functions allow for the insertion of one or many middleware,\n// We register internal middleware first in the stack, but have no way of controlling\n// the order that addMiddleware is run in service workers so need an internal function.\nexport function __facade_register__(...args: (Middleware | Middleware[])[]) {\n\t__facade_middleware__.push(...args.flat());\n}\nexport function __facade_registerInternal__(\n\t...args: (Middleware | Middleware[])[]\n) {\n\t__facade_middleware__.unshift(...args.flat());\n}\n\nfunction __facade_invokeChain__(\n\trequest: IncomingRequest,\n\tenv: any,\n\tctx: ExecutionContext,\n\tdispatch: Dispatcher,\n\tmiddlewareChain: Middleware[]\n): Awaitable {\n\tconst [head, ...tail] = middlewareChain;\n\tconst middlewareCtx: MiddlewareContext = {\n\t\tdispatch,\n\t\tnext(newRequest, newEnv) {\n\t\t\treturn __facade_invokeChain__(newRequest, newEnv, ctx, dispatch, tail);\n\t\t},\n\t};\n\treturn head(request, env, ctx, middlewareCtx);\n}\n\nexport function __facade_invoke__(\n\trequest: IncomingRequest,\n\tenv: any,\n\tctx: ExecutionContext,\n\tdispatch: Dispatcher,\n\tfinalMiddleware: Middleware\n): Awaitable {\n\treturn __facade_invokeChain__(request, env, ctx, dispatch, [\n\t\t...__facade_middleware__,\n\t\tfinalMiddleware,\n\t]);\n}\n", "// This loads all middlewares exposed on the middleware object and then starts\n// the invocation chain. The big idea is that we can add these to the middleware\n// export dynamically through wrangler, or we can potentially let users directly\n// add them as a sort of \"plugin\" system.\n\nimport ENTRY, { __INTERNAL_WRANGLER_MIDDLEWARE__ } from \"/home/ajith/projects/meterx-server/.wrangler/tmp/bundle-6hK2Wx/middleware-insertion-facade.js\";\nimport { __facade_invoke__, __facade_register__, Dispatcher } from \"/home/ajith/projects/meterx-server/node_modules/wrangler/templates/middleware/common.ts\";\nimport type { WorkerEntrypointConstructor } from \"/home/ajith/projects/meterx-server/.wrangler/tmp/bundle-6hK2Wx/middleware-insertion-facade.js\";\n\n// Preserve all the exports from the worker\nexport * from \"/home/ajith/projects/meterx-server/.wrangler/tmp/bundle-6hK2Wx/middleware-insertion-facade.js\";\n\nclass __Facade_ScheduledController__ implements ScheduledController {\n\treadonly #noRetry: ScheduledController[\"noRetry\"];\n\n\tconstructor(\n\t\treadonly scheduledTime: number,\n\t\treadonly cron: string,\n\t\tnoRetry: ScheduledController[\"noRetry\"]\n\t) {\n\t\tthis.#noRetry = noRetry;\n\t}\n\n\tnoRetry() {\n\t\tif (!(this instanceof __Facade_ScheduledController__)) {\n\t\t\tthrow new TypeError(\"Illegal invocation\");\n\t\t}\n\t\t// Need to call native method immediately in case uncaught error thrown\n\t\tthis.#noRetry();\n\t}\n}\n\nfunction wrapExportedHandler(worker: ExportedHandler): ExportedHandler {\n\t// If we don't have any middleware defined, just return the handler as is\n\tif (\n\t\t__INTERNAL_WRANGLER_MIDDLEWARE__ === undefined ||\n\t\t__INTERNAL_WRANGLER_MIDDLEWARE__.length === 0\n\t) {\n\t\treturn worker;\n\t}\n\t// Otherwise, register all middleware once\n\tfor (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) {\n\t\t__facade_register__(middleware);\n\t}\n\n\tconst fetchDispatcher: ExportedHandlerFetchHandler = function (\n\t\trequest,\n\t\tenv,\n\t\tctx\n\t) {\n\t\tif (worker.fetch === undefined) {\n\t\t\tthrow new Error(\"Handler does not export a fetch() function.\");\n\t\t}\n\t\treturn worker.fetch(request, env, ctx);\n\t};\n\n\treturn {\n\t\t...worker,\n\t\tfetch(request, env, ctx) {\n\t\t\tconst dispatcher: Dispatcher = function (type, init) {\n\t\t\t\tif (type === \"scheduled\" && worker.scheduled !== undefined) {\n\t\t\t\t\tconst controller = new __Facade_ScheduledController__(\n\t\t\t\t\t\tDate.now(),\n\t\t\t\t\t\tinit.cron ?? \"\",\n\t\t\t\t\t\t() => {}\n\t\t\t\t\t);\n\t\t\t\t\treturn worker.scheduled(controller, env, ctx);\n\t\t\t\t}\n\t\t\t};\n\t\t\treturn __facade_invoke__(request, env, ctx, dispatcher, fetchDispatcher);\n\t\t},\n\t};\n}\n\nfunction wrapWorkerEntrypoint(\n\tklass: WorkerEntrypointConstructor\n): WorkerEntrypointConstructor {\n\t// If we don't have any middleware defined, just return the handler as is\n\tif (\n\t\t__INTERNAL_WRANGLER_MIDDLEWARE__ === undefined ||\n\t\t__INTERNAL_WRANGLER_MIDDLEWARE__.length === 0\n\t) {\n\t\treturn klass;\n\t}\n\t// Otherwise, register all middleware once\n\tfor (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) {\n\t\t__facade_register__(middleware);\n\t}\n\n\t// `extend`ing `klass` here so other RPC methods remain callable\n\treturn class extends klass {\n\t\t#fetchDispatcher: ExportedHandlerFetchHandler> = (\n\t\t\trequest,\n\t\t\tenv,\n\t\t\tctx\n\t\t) => {\n\t\t\tthis.env = env;\n\t\t\tthis.ctx = ctx;\n\t\t\tif (super.fetch === undefined) {\n\t\t\t\tthrow new Error(\"Entrypoint class does not define a fetch() function.\");\n\t\t\t}\n\t\t\treturn super.fetch(request);\n\t\t};\n\n\t\t#dispatcher: Dispatcher = (type, init) => {\n\t\t\tif (type === \"scheduled\" && super.scheduled !== undefined) {\n\t\t\t\tconst controller = new __Facade_ScheduledController__(\n\t\t\t\t\tDate.now(),\n\t\t\t\t\tinit.cron ?? \"\",\n\t\t\t\t\t() => {}\n\t\t\t\t);\n\t\t\t\treturn super.scheduled(controller);\n\t\t\t}\n\t\t};\n\n\t\tfetch(request: Request) {\n\t\t\treturn __facade_invoke__(\n\t\t\t\trequest,\n\t\t\t\tthis.env,\n\t\t\t\tthis.ctx,\n\t\t\t\tthis.#dispatcher,\n\t\t\t\tthis.#fetchDispatcher\n\t\t\t);\n\t\t}\n\t};\n}\n\nlet WRAPPED_ENTRY: ExportedHandler | WorkerEntrypointConstructor | undefined;\nif (typeof ENTRY === \"object\") {\n\tWRAPPED_ENTRY = wrapExportedHandler(ENTRY);\n} else if (typeof ENTRY === \"function\") {\n\tWRAPPED_ENTRY = wrapWorkerEntrypoint(ENTRY);\n}\nexport default WRAPPED_ENTRY;\n"], + "mappings": ";;;;AAGA,IAAO,qBAA8C;AAAA,EACpD,MAAM,MAAM,SAAS,KAAK,SAAS;AAClC,UAAM,WAAW,MAAM,IAAI,OAAO,MAAM,QAAQ,KAAK,OAAO;AAC5D,WAAO,IAAI,SAAS,SAAS,MAAM,QAAQ;AAAA,EAC5C;AACD;;;ACNA,IAAM,YAAwB,8BAAO,SAAS,KAAK,MAAM,kBAAkB;AAC1E,MAAI;AACH,WAAO,MAAM,cAAc,KAAK,SAAS,GAAG;AAAA,EAC7C,UAAE;AACD,QAAI;AACH,UAAI,QAAQ,SAAS,QAAQ,CAAC,QAAQ,UAAU;AAC/C,cAAM,SAAS,QAAQ,KAAK,UAAU;AACtC,eAAO,EAAE,MAAM,OAAO,KAAK,GAAG,MAAM;AAAA,QAAC;AAAA,MACtC;AAAA,IACD,SAAS,GAAG;AACX,cAAQ,MAAM,4CAA4C,CAAC;AAAA,IAC5D;AAAA,EACD;AACD,GAb8B;AAe9B,IAAO,6CAAQ;;;ACRf,SAAS,YAAY,GAAmB;AACvC,SAAO;AAAA,IACN,MAAM,GAAG;AAAA,IACT,SAAS,GAAG,WAAW,OAAO,CAAC;AAAA,IAC/B,OAAO,GAAG;AAAA,IACV,OAAO,GAAG,UAAU,SAAY,SAAY,YAAY,EAAE,KAAK;AAAA,EAChE;AACD;AAPS;AAUT,IAAM,YAAwB,8BAAO,SAAS,KAAK,MAAM,kBAAkB;AAC1E,MAAI;AACH,WAAO,MAAM,cAAc,KAAK,SAAS,GAAG;AAAA,EAC7C,SAAS,GAAQ;AAChB,UAAM,QAAQ,YAAY,CAAC;AAC3B,WAAO,SAAS,KAAK,OAAO;AAAA,MAC3B,QAAQ;AAAA,MACR,SAAS,EAAE,+BAA+B,OAAO;AAAA,IAClD,CAAC;AAAA,EACF;AACD,GAV8B;AAY9B,IAAO,2CAAQ;;;ACzBJ,IAAM,mCAAmC;AAAA,EAE9B;AAAA,EAAyB;AAC3C;AACA,IAAO,sCAAQ;;;ACcnB,IAAM,wBAAsC,CAAC;AAKtC,SAAS,uBAAuB,MAAqC;AAC3E,wBAAsB,KAAK,GAAG,KAAK,KAAK,CAAC;AAC1C;AAFgB;AAShB,SAAS,uBACR,SACA,KACA,KACA,UACA,iBACsB;AACtB,QAAM,CAAC,MAAM,GAAG,IAAI,IAAI;AACxB,QAAM,gBAAmC;AAAA,IACxC;AAAA,IACA,KAAK,YAAY,QAAQ;AACxB,aAAO,uBAAuB,YAAY,QAAQ,KAAK,UAAU,IAAI;AAAA,IACtE;AAAA,EACD;AACA,SAAO,KAAK,SAAS,KAAK,KAAK,aAAa;AAC7C;AAfS;AAiBF,SAAS,kBACf,SACA,KACA,KACA,UACA,iBACsB;AACtB,SAAO,uBAAuB,SAAS,KAAK,KAAK,UAAU;AAAA,IAC1D,GAAG;AAAA,IACH;AAAA,EACD,CAAC;AACF;AAXgB;;;AC3ChB,IAAM,iCAAN,MAAM,gCAA8D;AAAA,EAGnE,YACU,eACA,MACT,SACC;AAHQ;AACA;AAGT,SAAK,WAAW;AAAA,EACjB;AAAA,EArBD,OAYoE;AAAA;AAAA;AAAA,EAC1D;AAAA,EAUT,UAAU;AACT,QAAI,EAAE,gBAAgB,kCAAiC;AACtD,YAAM,IAAI,UAAU,oBAAoB;AAAA,IACzC;AAEA,SAAK,SAAS;AAAA,EACf;AACD;AAEA,SAAS,oBAAoB,QAA0C;AAEtE,MACC,qCAAqC,UACrC,iCAAiC,WAAW,GAC3C;AACD,WAAO;AAAA,EACR;AAEA,aAAW,cAAc,kCAAkC;AAC1D,wBAAoB,UAAU;AAAA,EAC/B;AAEA,QAAM,kBAA+C,gCACpD,SACA,KACA,KACC;AACD,QAAI,OAAO,UAAU,QAAW;AAC/B,YAAM,IAAI,MAAM,6CAA6C;AAAA,IAC9D;AACA,WAAO,OAAO,MAAM,SAAS,KAAK,GAAG;AAAA,EACtC,GATqD;AAWrD,SAAO;AAAA,IACN,GAAG;AAAA,IACH,MAAM,SAAS,KAAK,KAAK;AACxB,YAAM,aAAyB,gCAAU,MAAM,MAAM;AACpD,YAAI,SAAS,eAAe,OAAO,cAAc,QAAW;AAC3D,gBAAM,aAAa,IAAI;AAAA,YACtB,KAAK,IAAI;AAAA,YACT,KAAK,QAAQ;AAAA,YACb,MAAM;AAAA,YAAC;AAAA,UACR;AACA,iBAAO,OAAO,UAAU,YAAY,KAAK,GAAG;AAAA,QAC7C;AAAA,MACD,GAT+B;AAU/B,aAAO,kBAAkB,SAAS,KAAK,KAAK,YAAY,eAAe;AAAA,IACxE;AAAA,EACD;AACD;AAxCS;AA0CT,SAAS,qBACR,OAC8B;AAE9B,MACC,qCAAqC,UACrC,iCAAiC,WAAW,GAC3C;AACD,WAAO;AAAA,EACR;AAEA,aAAW,cAAc,kCAAkC;AAC1D,wBAAoB,UAAU;AAAA,EAC/B;AAGA,SAAO,cAAc,MAAM;AAAA,IAC1B,mBAAyE,wBACxE,SACA,KACA,QACI;AACJ,WAAK,MAAM;AACX,WAAK,MAAM;AACX,UAAI,MAAM,UAAU,QAAW;AAC9B,cAAM,IAAI,MAAM,sDAAsD;AAAA,MACvE;AACA,aAAO,MAAM,MAAM,OAAO;AAAA,IAC3B,GAXyE;AAAA,IAazE,cAA0B,wBAAC,MAAM,SAAS;AACzC,UAAI,SAAS,eAAe,MAAM,cAAc,QAAW;AAC1D,cAAM,aAAa,IAAI;AAAA,UACtB,KAAK,IAAI;AAAA,UACT,KAAK,QAAQ;AAAA,UACb,MAAM;AAAA,UAAC;AAAA,QACR;AACA,eAAO,MAAM,UAAU,UAAU;AAAA,MAClC;AAAA,IACD,GAT0B;AAAA,IAW1B,MAAM,SAAwD;AAC7D,aAAO;AAAA,QACN;AAAA,QACA,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK;AAAA,MACN;AAAA,IACD;AAAA,EACD;AACD;AAnDS;AAqDT,IAAI;AACJ,IAAI,OAAO,wCAAU,UAAU;AAC9B,kBAAgB,oBAAoB,mCAAK;AAC1C,WAAW,OAAO,wCAAU,YAAY;AACvC,kBAAgB,qBAAqB,mCAAK;AAC3C;AACA,IAAO,kCAAQ;", + "names": [] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d6217b..4133640 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,9 @@ All notable changes to MeterX are documented here. The format follows [Keep a Ch - `npm run package` builds one upload for Chrome, Edge and Opera, one for Firefox, and a source archive - The site is now several pages (Home, How it works, Features, Questions, About) and runs the extension's own popup, side panel and settings with sample data; the home page plays a test as you scroll +- Get MeterX page: every store on the MeterX dial with the needle pointing at the visitor's browser, live and upcoming stores (Chrome Web Store with Brave, Vivaldi and Arc, Firefox for Android, Safari) with their logos. `/get/?go` still jumps straight to the right store +- Questions show the open answer in a panel beside the list on wide screens; How it works compares ping under load on a grade C line and a grade A line + ### Fixed - Empty figures show a short muted bar with the unit hidden, instead of a stray hyphen next to "Mbps" - The side panel no longer scrolls when an ISP plan is set: plan rows show the percentage on one line, the status moved into the Results header, and the chart shrinks on shorter panels diff --git a/apps/site/build.mjs b/apps/site/build.mjs index ae9bbf1..a6a9d3f 100644 --- a/apps/site/build.mjs +++ b/apps/site/build.mjs @@ -34,7 +34,7 @@ for (const f of walk(dist).filter(p => p.endsWith('.html'))) { } const common = { absWorkingDir: here, bundle: true, target: 'es2022', minify: true }; -await build({ ...common, entryPoints: { home: 'src/home.ts', share: 'src/share.ts', get: 'src/get.ts', common: 'src/common.ts' }, format: 'esm', outdir: resolve(dist, 'js') }); +await build({ ...common, entryPoints: { home: 'src/home.ts', share: 'src/share.ts', get: 'src/get.ts', faq: 'src/faq.ts', common: 'src/common.ts' }, format: 'esm', outdir: resolve(dist, 'js') }); await build({ ...common, entryPoints: { 'demo-shim': 'src/demo-shim.ts' }, format: 'iife', outdir: resolve(dist, 'js') }); console.log('Site built to dist/'); diff --git a/apps/site/src/common.ts b/apps/site/src/common.ts index da4b175..2eb8ab1 100644 --- a/apps/site/src/common.ts +++ b/apps/site/src/common.ts @@ -40,8 +40,19 @@ function chrome(): void { onScroll(); } +/** Frames marked data-fit take the height their page reports, so the frame never scrolls on its own. */ +function fitFrames(): void { + addEventListener('message', e => { + if (e.origin !== location.origin || e.data?.mxDemo !== 'height') return; + document.querySelectorAll('iframe[data-fit]').forEach(f => { + if (f.contentWindow === e.source) f.style.height = `${e.data.h}px`; + }); + }); +} + wireInstall(); entrances(); chrome(); +fitFrames(); const year = document.getElementById('year'); if (year) year.textContent = String(new Date().getFullYear()); diff --git a/apps/site/src/demo-shim.ts b/apps/site/src/demo-shim.ts index 5202e7b..e7e5f8b 100644 --- a/apps/site/src/demo-shim.ts +++ b/apps/site/src/demo-shim.ts @@ -5,7 +5,7 @@ * @license AGPL-3.0 */ -import type { BroadcastMessage, HistoryEntry } from '@meterx/shared'; +import { VERSION, WORKER_URL, type BroadcastMessage, type HistoryEntry } from '@meterx/shared'; import { SAMPLE_PLAN, sampleFrame, sampleHistory, sampleResult } from './sample'; type Area = 'local' | 'sync'; @@ -96,3 +96,23 @@ window.addEventListener('message', e => { clearInterval(timer); show(e.data.p, false); }); + +// The demo answers the test server itself, so the pages show a connected state without touching the network. +const realFetch = window.fetch.bind(window); +window.fetch = (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input instanceof Request ? input.url : input); + if (!url.startsWith(WORKER_URL)) return realFetch(input, init); + const path = new URL(url).pathname; + const body = path === '/health' ? JSON.stringify({ status: 'ok', server: 'cloudflare-worker', version: VERSION, edge: 'MAA', timestamp: Date.now() }) : 'Pong!'; + return new Promise(resolve => setTimeout(() => resolve(new Response(body, { status: 200, headers: { 'Content-Type': path === '/health' ? 'application/json' : 'text/plain' } })), 18)); +}; + +// On the site, pages sit inside a frame: tighter padding, and the frame takes the page's height so only the site scrolls. +document.addEventListener('DOMContentLoaded', () => { + const style = document.createElement('style'); + style.textContent = '.settings .wrap{max-width:none;padding:8px 20px 20px}.settings #closeBtn{visibility:hidden}'; + document.head.append(style); + const report = () => parent.postMessage({ mxDemo: 'height', h: Math.ceil(document.documentElement.scrollHeight) }, location.origin); + new ResizeObserver(report).observe(document.body); + report(); +}); diff --git a/apps/site/src/faq.ts b/apps/site/src/faq.ts new file mode 100644 index 0000000..7630aef --- /dev/null +++ b/apps/site/src/faq.ts @@ -0,0 +1,23 @@ +/** + * Questions: on wide screens the open answer shows in a panel beside the list; on phones they stay a plain accordion. + * @license AGPL-3.0 + */ + +const list = document.getElementById('faq'), panel = document.getElementById('faqAnswer'); +const wide = matchMedia('(min-width: 900px)'); + +if (list && panel) { + const items = [...list.querySelectorAll('details')]; + const show = (d: HTMLDetailsElement) => { + items.forEach(x => x.classList.toggle('is-current', x === d)); + const more = d.dataset.more ? `Read more` : ''; + panel.querySelector('.answer')!.innerHTML = `

          ${d.querySelector('summary')!.textContent}

          ${d.querySelector('p')!.outerHTML}${more}`; + }; + items.forEach(d => d.querySelector('summary')!.addEventListener('click', e => { + if (!wide.matches) return; + e.preventDefault(); // on wide screens the answer goes to the panel instead of opening in place + show(d); + })); + document.body.classList.add('faq-js'); + show(items[0]); +} diff --git a/apps/site/src/get.ts b/apps/site/src/get.ts index a403713..85f4981 100644 --- a/apps/site/src/get.ts +++ b/apps/site/src/get.ts @@ -1,11 +1,56 @@ /** - * /get: one link to share anywhere; sends each visitor to their own browser's store. + * Get MeterX: every store on the brand's dial, with the needle pointing at the visitor's own browser. + * /get/?go skips the page and goes straight to the visitor's store, for links in posts and bios. * @license AGPL-3.0 */ -import { STORES, detectStore } from './stores'; +import { PLACES, STORES, detectStore } from './stores'; const id = detectStore(navigator.userAgent); -const url = id ? STORES[id].url : ''; -if (url) location.replace(url); -else document.getElementById('fallback')!.hidden = false; +const mine = id ? STORES[id] : null; +if (new URLSearchParams(location.search).has('go') && mine?.url) location.replace(mine.url); + +const $ = (i: string) => document.getElementById(i)!; +const logo = (p: (typeof PLACES)[number]) => + ``; + +// The dial: one stop per place along the logo's three-quarter arc, live ones in colour. +const dial = $('dial'); +const from = 135, sweep = 270, step = sweep / (PLACES.length - 1); +PLACES.forEach((p, i) => { + const a = ((from + i * step) * Math.PI) / 180; + const el = document.createElement('a'); + el.className = `stop${p.url ? ' live' : ''}${p.id === id ? ' mine' : ''}`; + el.href = p.url || '#next'; + el.style.left = `${50 + 46 * Math.cos(a)}%`; + el.style.top = `${50 + 46 * Math.sin(a)}%`; + el.title = `${p.store}: ${p.url ? 'available' : p.note}`; + el.innerHTML = logo(p); + dial.append(el); +}); + +// The needle swings to the visitor's browser, or rests at the top when the browser is unknown. +const target = PLACES.findIndex(p => p.id === id); +const angle = target >= 0 ? from + target * step : 270; +requestAnimationFrame(() => $("needle").style.setProperty("--a", `${angle - 90}deg`)); + +if (mine) { + const main = $('getMain') as HTMLAnchorElement; + const place = PLACES.find(p => p.id === id)!; + if (mine.url) { + main.href = mine.url; + main.textContent = `Add to ${mine.name}, free`; + $('getLine').textContent = `You are on ${mine.name}. One click installs it from ${place.store}.`; + } else { + $('getLine').textContent = `You are on ${mine.name}. ${place.note}; Firefox and Edge have it today.`; + } +} + +const card = (p: (typeof PLACES)[number]) => `
        • + ${logo(p)} +

          ${p.store}

          ${p.browsers}

          ${p.url ? '' : `${p.note}`}
          + ${p.url ? `Install` : ''} +
        • `; +$('storesNow').innerHTML = PLACES.filter(p => p.url).map(card).join(''); +$('storesNext').innerHTML = PLACES.filter(p => !p.url).map(card).join(''); +$('storesNext').closest('section')!.id = 'next'; diff --git a/apps/site/src/stores.ts b/apps/site/src/stores.ts index 32b17d4..362da7f 100644 --- a/apps/site/src/stores.ts +++ b/apps/site/src/stores.ts @@ -1,5 +1,5 @@ /** - * Where MeterX is listed. An empty url means the listing is not live yet; buttons then point at the install list. + * Where MeterX is listed and where it is going. An empty url means the listing is not live yet. * @license AGPL-3.0 */ @@ -8,10 +8,23 @@ export type StoreId = 'firefox' | 'edge' | 'opera' | 'chrome'; export const STORES: Record = { firefox: { name: 'Firefox', url: 'https://addons.mozilla.org/firefox/addon/meterx-by-ajithakdev/' }, edge: { name: 'Edge', url: 'https://microsoftedge.microsoft.com/addons/detail/glkpolbkpachomgmmhdkmjnghejbhlfc' }, - opera: { name: 'Opera', url: '' }, // shortcut: fill in from the Opera Add-ons listing + opera: { name: 'Opera', url: '' }, // shortcut: fill in from the Opera Add-ons listing once it is public chrome: { name: 'Chrome', url: '' }, // not listed yet }; +/** Every place on the Get page, in dial order. `browsers` are the ones that install from that store. */ +export interface Place { id: string; store: string; browsers: string; logo: string; colour: string; url: string; note: string } +export const PLACES: Place[] = [ + { id: 'firefox', store: 'Firefox Add-ons', browsers: 'Firefox on Windows, macOS and Linux', logo: 'firefoxbrowser', colour: '#FF7139', url: STORES.firefox.url, note: 'Live' }, + { id: 'edge', store: 'Microsoft Edge Add-ons', browsers: 'Microsoft Edge', logo: 'microsoftedge', colour: '#0C8DD7', url: STORES.edge.url, note: 'Live' }, + { id: 'opera', store: 'Opera Add-ons', browsers: 'Opera and Opera GX', logo: 'opera', colour: '#FF1B2D', url: STORES.opera.url, note: 'Listing link coming soon' }, + { id: 'chrome', store: 'Chrome Web Store', browsers: 'Chrome, plus Brave, Vivaldi and Arc', logo: 'googlechrome', colour: '#4285F4', url: STORES.chrome.url, note: 'Planned for the next release' }, + { id: 'brave', store: 'Brave', browsers: 'Installs from the Chrome Web Store', logo: 'brave', colour: '#FB542B', url: '', note: 'With the Chrome listing' }, + { id: 'vivaldi', store: 'Vivaldi', browsers: 'Installs from the Chrome Web Store', logo: 'vivaldi', colour: '#EF3939', url: '', note: 'With the Chrome listing' }, + { id: 'android', store: 'Firefox for Android', browsers: 'Firefox on Android phones and tablets', logo: 'firefoxbrowser', colour: '#FF7139', url: '', note: 'Planned' }, + { id: 'safari', store: 'Safari', browsers: 'Safari on macOS and iOS', logo: 'safari', colour: '#006CFF', url: '', note: 'Later: needs an Apple developer account' }, +]; + /** The visitor's browser, from the user agent. Order matters: Edge and Opera also say "Chrome". */ export function detectStore(ua: string): StoreId | null { if (/Firefox\//.test(ua)) return 'firefox'; diff --git a/apps/site/static/_partials/footer.html b/apps/site/static/_partials/footer.html index 1552111..ad8b6cd 100644 --- a/apps/site/static/_partials/footer.html +++ b/apps/site/static/_partials/footer.html @@ -1,6 +1,6 @@
          - MeterX + MeterX
          -

          Where the twenty seconds go

          Segments are sized by how long each phase takes.

          - +

          Where the twenty seconds go

          Each bar shows the phase's share of the whole test.

            -
          • Connect

            MeterX opens a connection to the nearest Cloudflare location, or to your own server. Under a second.

          • -
          • Ping and jitter

            A run of small requests measures the delay and how much it wobbles from one request to the next. Requests that never return count as loss.

          • -
          • Download

            Four parallel streams fill your line for a fixed time. The slow start and the last moments are trimmed, so the figure is the steady speed.

          • -
          • Upload

            Three streams do the same upward while pings keep running, to catch the delay that builds under load.

          • +
          • Under a second

            Connect

            MeterX opens a connection to the nearest Cloudflare location, or to your own server.

          • +
          • 2 seconds

            Ping and jitter

            A run of small requests measures the delay and how much it wobbles from one request to the next. Requests that never return count as loss.

          • +
          • About 9 seconds

            Download

            Four parallel streams fill your line for a fixed time. The slow start and the last moments are trimmed, so the figure is the steady speed.

          • +
          • About 9 seconds

            Upload

            Three streams do the same upward while pings keep running, to catch the delay that builds under load.

          -
          +
          +
          +

          Latency under load

          A line can look fast and still stutter on a call while something downloads. That is bufferbloat: routers queue too much data, so every request waits in line.

          MeterX keeps pinging during the download and upload and grades the worst rise above the ping at rest.

          @@ -56,6 +52,36 @@

          How MeterX measures

          FMore than 200 msEverything lags when the line is busy
          +
          +
          +
          + + Ping during a sample test: about 19 ms at rest, rising to 61 ms while downloading and 46 ms while uploading + + + 0255075 + + + RestDownloadingUploading + +42 ms under load + +
          Ping in ms during a sample test. The rise of 42 ms gives grade C.
          +
          +
          + + Ping during a sample test on a line with good queue management: about 19 ms at rest and 23 ms under load + + + 0255075 + + + RestDownloadingUploading + +4 ms under load + +
          The same test on a line with good queue management: ping barely moves, grade A.
          +
          +
          +
          diff --git a/apps/site/static/img/glyph-on-dark.svg b/apps/site/static/img/glyph-on-dark.svg new file mode 100644 index 0000000..bd6ac81 --- /dev/null +++ b/apps/site/static/img/glyph-on-dark.svg @@ -0,0 +1 @@ + diff --git a/apps/site/static/img/glyph-on-light.svg b/apps/site/static/img/glyph-on-light.svg new file mode 100644 index 0000000..dbab0d3 --- /dev/null +++ b/apps/site/static/img/glyph-on-light.svg @@ -0,0 +1 @@ + diff --git a/apps/site/static/img/stores/brave.svg b/apps/site/static/img/stores/brave.svg new file mode 100644 index 0000000..fa798a4 --- /dev/null +++ b/apps/site/static/img/stores/brave.svg @@ -0,0 +1 @@ +Brave \ No newline at end of file diff --git a/apps/site/static/img/stores/firefoxbrowser.svg b/apps/site/static/img/stores/firefoxbrowser.svg new file mode 100644 index 0000000..3cbc89c --- /dev/null +++ b/apps/site/static/img/stores/firefoxbrowser.svg @@ -0,0 +1 @@ +Firefox Browser \ No newline at end of file diff --git a/apps/site/static/img/stores/googlechrome.svg b/apps/site/static/img/stores/googlechrome.svg new file mode 100644 index 0000000..919ac2a --- /dev/null +++ b/apps/site/static/img/stores/googlechrome.svg @@ -0,0 +1 @@ +Google Chrome \ No newline at end of file diff --git a/apps/site/static/img/stores/microsoftedge.svg b/apps/site/static/img/stores/microsoftedge.svg new file mode 100644 index 0000000..2b0cd03 --- /dev/null +++ b/apps/site/static/img/stores/microsoftedge.svg @@ -0,0 +1 @@ +Microsoft Edge \ No newline at end of file diff --git a/apps/site/static/img/stores/opera.svg b/apps/site/static/img/stores/opera.svg new file mode 100644 index 0000000..d1194ee --- /dev/null +++ b/apps/site/static/img/stores/opera.svg @@ -0,0 +1 @@ +Opera \ No newline at end of file diff --git a/apps/site/static/img/stores/safari.svg b/apps/site/static/img/stores/safari.svg new file mode 100644 index 0000000..5d15438 --- /dev/null +++ b/apps/site/static/img/stores/safari.svg @@ -0,0 +1 @@ +Safari \ No newline at end of file diff --git a/apps/site/static/img/stores/vivaldi.svg b/apps/site/static/img/stores/vivaldi.svg new file mode 100644 index 0000000..167e8ad --- /dev/null +++ b/apps/site/static/img/stores/vivaldi.svg @@ -0,0 +1 @@ +Vivaldi \ No newline at end of file diff --git a/apps/site/static/styles/get.css b/apps/site/static/styles/get.css new file mode 100644 index 0000000..63a9a58 --- /dev/null +++ b/apps/site/static/styles/get.css @@ -0,0 +1,45 @@ +/* Get MeterX: the stores sit on the logo's dial and the needle points at the visitor's browser. */ + +.get-hero { position: relative; display: grid; place-items: center; width: min(640px, calc(100vw - 2 * var(--m))); aspect-ratio: 1; margin: 48px auto 0; } +.dial { position: absolute; inset: 0; } +.dial-arc { position: absolute; inset: 4%; width: 92%; height: 92%; } +.dial-arc path { fill: none; stroke: var(--control); stroke-width: 1.2; stroke-linecap: round; stroke-dasharray: 0.6 2.4; } + +/* The needle: from the centre toward the visitor's store, in the brand blue. */ +.needle { position: absolute; left: 50%; top: 50%; width: 0; height: 40%; transform-origin: 50% 0; transform: rotate(var(--a, 180deg)); transition: transform 1.1s cubic-bezier(.2, 1.4, .4, 1); } +/* Only the outer part of the needle is drawn, so it points at the store without crossing the words in the middle. */ +.needle::before { content: ""; position: absolute; left: -1.5px; top: 62%; width: 3px; height: 26%; border-radius: 2px; background: var(--data); } +.needle::after { content: ""; position: absolute; left: -5px; top: calc(62% - 5px); width: 10px; height: 10px; border-radius: 50%; background: var(--data); } +@media (prefers-reduced-motion: reduce) { .needle { transition: none; } } + +.stop { position: absolute; width: 64px; height: 64px; margin: -32px 0 0 -32px; display: grid; place-items: center; border-radius: 18px; background: var(--surface); box-shadow: 0 0 0 1px var(--border); transition: transform 160ms var(--ease), box-shadow 160ms var(--ease); } +.stop:hover { transform: scale(1.08); } +.stop .logo { width: 32px; height: 32px; background: var(--muted); opacity: .45; } +.stop.live .logo { background: var(--c); opacity: 1; } +.stop.mine { box-shadow: 0 0 0 2px var(--data), 0 12px 32px -12px var(--data); transform: scale(1.12); } + +.logo { display: block; flex: none; -webkit-mask: var(--m) center / contain no-repeat; mask: var(--m) center / contain no-repeat; background: var(--c); } + +.get-center { position: relative; display: grid; justify-items: center; gap: 16px; text-align: center; max-width: 340px; } +.get-center img { width: 72px; height: 72px; } +.get-center .lede { font-size: 17px; } + +@media (max-width: 560px) { + .stop { width: 44px; height: 44px; margin: -22px 0 0 -22px; border-radius: 12px; } + .stop .logo { width: 22px; height: 22px; } + .get-center { max-width: 210px; gap: 10px; } + .get-center img { width: 44px; height: 44px; } + .get-center h1 { font-size: 30px; } + .get-center .lede { font-size: 14px; line-height: 1.4; } + .get-center .cta { height: 40px; padding: 0 16px; font-size: 14px; } +} + +/* Store cards */ +.stores { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr)); gap: 12px; } +.store { display: flex; align-items: center; gap: 16px; padding: 20px; border-radius: 14px; background: var(--surface); box-shadow: 0 0 0 1px var(--border); } +.store .logo { width: 40px; height: 40px; } +.store > div { flex: 1; min-width: 0; display: grid; gap: 2px; } +.store p { font-size: var(--t-small); color: var(--muted); } +.store.mine { box-shadow: 0 0 0 2px var(--data); } +.stores.soon .logo { background: var(--muted); opacity: .6; } +.stores.soon .pill { justify-self: start; margin-top: 6px; white-space: normal; height: auto; padding: 3px 10px; } diff --git a/apps/site/static/styles/site.css b/apps/site/static/styles/site.css index 9975727..51720b5 100644 --- a/apps/site/static/styles/site.css +++ b/apps/site/static/styles/site.css @@ -54,7 +54,8 @@ body.site { font: 400 var(--t-body)/1.6 var(--font); background: var(--ground); .site-bar.is-scrolled { border-bottom-color: var(--border); } .site-bar .in { display: flex; align-items: center; gap: 32px; height: 68px; padding: 0 var(--m); } .brand { display: inline-flex; align-items: center; gap: 10px; font-weight: 600; font-size: 18px; color: var(--text); text-decoration: none; } -.brand img { border-radius: 7px; } +.brand img { display: block; width: 36px; height: 36px; } +.site-foot .brand img { width: 28px; height: 28px; } .links { display: flex; gap: 4px; margin-right: auto; } .links a { padding: 8px 12px; border-radius: 8px; color: var(--muted); text-decoration: none; font-size: 15px; transition: color 120ms var(--ease), background-color 120ms var(--ease); } .links a:hover { color: var(--text); background: var(--raised); } @@ -87,8 +88,9 @@ main { display: block; } .device { border: 0; display: block; background: var(--ground); border-radius: 14px; box-shadow: 0 0 0 1px var(--border), 0 24px 48px -24px rgb(0 0 0 / .35); color-scheme: light dark; } .device.popup { width: 360px; height: 594px; } .device.panel { width: 420px; height: 760px; } -.device.options { width: 100%; height: 660px; } -@media (max-width: 900px) { .device.options { height: 1500px; } } +.device.options { width: 100%; height: 620px; } +/* The settings page pads its content by 20px; the caption lines up with that, not with the frame edge. */ +.device.options + .demo-note { padding-left: 20px; } .demo-note { margin-top: 12px; font-size: var(--t-small); color: var(--muted); } /* Split: words on one side, the product on the other. */ @@ -118,15 +120,31 @@ ul.clean { list-style: none; margin: 0; padding: 0; } [data-enter] > * { transition: opacity 600ms var(--ease) calc(var(--i, 0) * 70ms), transform 600ms var(--ease) calc(var(--i, 0) * 70ms); } @media (prefers-reduced-motion: reduce) { .pre { opacity: 1; transform: none; } } -/* How it works: the progress bar enlarged, segments sized by real phase length. */ -.phase-bar { display: flex; gap: 6px; height: 64px; } -.phase-bar > div { border-radius: 6px; background: var(--raised); display: flex; align-items: center; padding: 0 16px; min-width: 10px; overflow: hidden; white-space: nowrap; font-size: var(--t-small); } -.phase-bar > .dl { background: var(--data); color: var(--on-data); } -.phase-bar > .ul { background: var(--control); } -.steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 32px; margin-top: 32px; } +/* How it works: each step carries a bar showing its share of the whole test. */ +.steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 32px; } .steps li { display: grid; gap: 8px; align-content: start; } +.dur-bar { display: block; height: 10px; border-radius: 5px; background: var(--raised); overflow: hidden; margin-bottom: 8px; } +.dur-bar i { display: block; height: 100%; border-radius: 5px; background: var(--muted); } +.dur-bar.dl i { background: var(--data); } +.dur { font-size: var(--t-small); color: var(--muted); font-variant-numeric: tabular-nums; } @media (max-width: 900px) { .steps { grid-template-columns: 1fr 1fr; } } -@media (max-width: 560px) { .steps { grid-template-columns: 1fr; } .phase-bar span { display: none; } } +@media (max-width: 560px) { .steps { grid-template-columns: 1fr; } } + +/* Latency under load: words and table beside a sample ping chart. */ +.load-split { align-items: start; } +.charts { display: grid; gap: 32px; width: min(440px, 100%); } +.ping-chart { margin: 0; } +.ping-chart svg { display: block; width: 100%; height: auto; border-radius: 14px; background: var(--surface); box-shadow: 0 0 0 1px var(--border); } +.ping-chart .band { fill: transparent; } +.ping-chart .band.dl { fill: color-mix(in srgb, var(--data) 9%, transparent); } +.ping-chart .band.ul { fill: color-mix(in srgb, var(--muted) 9%, transparent); } +.ping-chart .grid line { stroke: var(--rule); stroke-width: 1; } +.ping-chart .axis text { fill: var(--muted); font-size: 11px; text-anchor: end; } +.ping-chart .labels text { fill: var(--muted); font-size: 12px; text-anchor: middle; } +.ping-chart .rest { stroke: var(--muted); stroke-dasharray: 4 4; stroke-width: 1; } +.ping-chart .ping { fill: none; stroke: var(--data); stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; } +.ping-chart .ping.good { stroke: var(--positive); } +.ping-chart .note { fill: var(--text); font-size: 13px; font-weight: 600; } .grades { width: 100%; max-width: 760px; border-collapse: collapse; margin-top: 32px; } .grades caption { text-align: left; padding-bottom: 12px; } @@ -138,17 +156,38 @@ ul.clean { list-style: none; margin: 0; padding: 0; } @media (max-width: 760px) { .facts.two { grid-template-columns: 1fr; } } .facts li > span { color: var(--muted); } -/* Questions */ -.faq { display: grid; gap: 8px; max-width: calc(760px + 2 * var(--m)); margin-top: 56px; } -.faq details { border-radius: 10px; padding: 0 20px; background: var(--surface); box-shadow: 0 0 0 1px var(--border); } -.faq summary { list-style: none; cursor: pointer; display: flex; justify-content: space-between; gap: 24px; align-items: center; font-size: 18px; font-weight: 500; padding: 18px 0; } +/* Questions: list on the left, the open answer beside it on wide screens; an accordion on phones. */ +.faq-layout { display: grid; grid-template-columns: minmax(0, 460px) minmax(0, 1fr); gap: 48px; align-items: start; margin-top: 56px; } +.faq { display: grid; gap: 8px; } +.faq details { border-radius: 10px; padding: 0 18px; background: var(--surface); box-shadow: 0 0 0 1px var(--border); transition: box-shadow 160ms var(--ease); } +.faq summary { list-style: none; cursor: pointer; display: flex; justify-content: space-between; gap: 16px; align-items: center; font-size: 17px; font-weight: 500; padding: 16px 0; } .faq summary::-webkit-details-marker { display: none; } -.faq summary::after { content: ""; width: 9px; height: 9px; border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted); transform: rotate(45deg) translateY(-3px); flex: none; transition: transform 160ms var(--ease); } +.faq summary::after { content: ""; width: 8px; height: 8px; border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted); transform: rotate(45deg) translateY(-3px); flex: none; transition: transform 160ms var(--ease); } .faq details[open] summary::after { transform: rotate(225deg) translateY(-3px); } -.faq details p { padding: 0 0 20px; color: var(--muted); } +.faq details p { padding: 0 0 18px; color: var(--muted); } +.faq-side .faq-answer { display: none; } +.faq-help { display: grid; gap: 12px; padding: 24px 32px; border-radius: 14px; background: var(--raised); } +.faq-help h2 { font-size: 20px !important; letter-spacing: -0.01em; } +.faq-help p { color: var(--muted); } +.faq-help .row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 4px; } +.faq-help .ghost { background: var(--surface); } +@media (min-width: 900px) { + .faq-js .faq details p { display: none; } + .faq-js .faq summary::after { transform: rotate(-45deg); } + .faq-js .faq details.is-current { box-shadow: 0 0 0 2px var(--data); } + .faq-js .faq-side { display: grid; gap: 16px; position: sticky; top: calc(var(--bar-h) + 24px); } + .faq-js .faq-answer { display: block; padding: 32px; border-radius: 14px; background: var(--surface); box-shadow: 0 0 0 1px var(--border); min-height: 240px; } + .faq-answer .answer { display: grid; gap: 16px; justify-items: start; } + .faq-answer .more { font-size: 15px; } + .faq-answer h2 { font-size: 26px; letter-spacing: -0.02em; } + .faq-answer p { font-size: var(--t-lede); line-height: 1.6; color: var(--text); } +} +@media (max-width: 899px) { .faq-layout { grid-template-columns: 1fr; } } /* A full report page as a picture: the whole page, never cropped. */ -.device-wrap.report { width: min(440px, 100%); } +.split.report-split { grid-template-columns: minmax(0, 1fr) minmax(0, 440px); } +@media (max-width: 900px) { .split.report-split { grid-template-columns: 1fr; } } +.device-wrap.report { width: 100%; max-width: 440px; } .device-wrap.report img { display: block; width: 100%; height: auto; border-radius: 10px; box-shadow: 0 0 0 1px var(--border), 0 24px 48px -24px rgb(0 0 0 / .35); } /* Share viewer result card */ From cff64168b6096b61c68c1f1e2da525a3417544db Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Sat, 24 Oct 2026 15:26:42 +0530 Subject: [PATCH 23/44] feat(site): contribute page with live GitHub issues and contributors Live repo stats, open good first issue and help wanted issues, and the contributors list come from the public GitHub API, cached for the visit, with plain links when the API is unavailable. The CSP allows the API and avatar hosts. CONTRIBUTING.md shows the current layout, and the logo link reads as MeterX home. --- CHANGELOG.md | 2 + CONTRIBUTING.md | 12 ++-- RESOURCES-AND-GUIDE.md | 1 + apps/site/build.mjs | 2 +- apps/site/src/contribute.ts | 39 ++++++++++++ apps/site/src/github.test.ts | 22 +++++++ apps/site/src/github.ts | 38 ++++++++++++ apps/site/static/_headers | 2 +- apps/site/static/_partials/footer.html | 1 + apps/site/static/_partials/header.html | 2 +- apps/site/static/contribute/index.html | 82 ++++++++++++++++++++++++++ apps/site/static/sitemap.xml | 1 + apps/site/static/styles/site.css | 33 +++++++++++ docs/CODEMAP.md | 9 +-- docs/SEO.md | 2 +- 15 files changed, 236 insertions(+), 12 deletions(-) create mode 100644 apps/site/src/contribute.ts create mode 100644 apps/site/src/github.test.ts create mode 100644 apps/site/src/github.ts create mode 100644 apps/site/static/contribute/index.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 4133640..57efa37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ All notable changes to MeterX are documented here. The format follows [Keep a Ch - Get MeterX page: every store on the MeterX dial with the needle pointing at the visitor's browser, live and upcoming stores (Chrome Web Store with Brave, Vivaldi and Arc, Firefox for Android, Safari) with their logos. `/get/?go` still jumps straight to the right store - Questions show the open answer in a panel beside the list on wide screens; How it works compares ping under load on a grade C line and a grade A line +- Contribute page with live repo stats, issues for newcomers and contributors from GitHub, plus a four step setup + ### Fixed - Empty figures show a short muted bar with the unit hidden, instead of a stray hyphen next to "Mbps" - The side panel no longer scrolls when an ISP plan is set: plan rows show the percentage on one line, the status moved into the Results header, and the chart shrinks on shorter panels diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a068230..ef811d2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,12 +40,16 @@ Starts the Express.js self-host server. Useful for LAN testing or when you don't ## Project Structure ```text -packages/ - extension/ # Browser extension (TypeScript, Manifest V3, esbuild) - worker/ # Cloudflare Worker — primary serverless backend - server/ # Express.js server — self-host fallback +apps/extension Browser extension (TypeScript, Manifest V3, esbuild) +apps/site meterx.ajithakdev.com +services/worker Cloudflare Worker, the default test server +services/server Express server, for self-hosting +packages/shared Types, API constants and rules every part shares +packages/reports PDF, CSV and JSON builders ``` +The full map is in [docs/CODEMAP.md](docs/CODEMAP.md). + ## Commands | Command | What it does | diff --git a/RESOURCES-AND-GUIDE.md b/RESOURCES-AND-GUIDE.md index 8448fa7..b6531bf 100644 --- a/RESOURCES-AND-GUIDE.md +++ b/RESOURCES-AND-GUIDE.md @@ -18,6 +18,7 @@ Every link for MeterX, what it is for, and how to run, package and release it. P | --- | --- | | https://meterx.ajithakdev.com | Product site: home, how it works, features, questions, about | | https://meterx.ajithakdev.com/get/ | Sends each visitor to their own browser's store. Use this link in posts and bios | +| https://meterx.ajithakdev.com/contribute/ | Contribute: live issues for newcomers and contributors from GitHub | | https://meterx.ajithakdev.com/privacy/ | Privacy policy (the stores link here) | | https://meterx.ajithakdev.com/r/ | Share viewer: opens links made by the Share button | | https://addons.mozilla.org/firefox/addon/meterx-by-ajithakdev/ | Firefox listing (add-on id `meterx@ajithakdev`) | diff --git a/apps/site/build.mjs b/apps/site/build.mjs index a6a9d3f..e0dd161 100644 --- a/apps/site/build.mjs +++ b/apps/site/build.mjs @@ -34,7 +34,7 @@ for (const f of walk(dist).filter(p => p.endsWith('.html'))) { } const common = { absWorkingDir: here, bundle: true, target: 'es2022', minify: true }; -await build({ ...common, entryPoints: { home: 'src/home.ts', share: 'src/share.ts', get: 'src/get.ts', faq: 'src/faq.ts', common: 'src/common.ts' }, format: 'esm', outdir: resolve(dist, 'js') }); +await build({ ...common, entryPoints: { home: 'src/home.ts', share: 'src/share.ts', get: 'src/get.ts', faq: 'src/faq.ts', contribute: 'src/contribute.ts', common: 'src/common.ts' }, format: 'esm', outdir: resolve(dist, 'js') }); await build({ ...common, entryPoints: { 'demo-shim': 'src/demo-shim.ts' }, format: 'iife', outdir: resolve(dist, 'js') }); console.log('Site built to dist/'); diff --git a/apps/site/src/contribute.ts b/apps/site/src/contribute.ts new file mode 100644 index 0000000..681dbba --- /dev/null +++ b/apps/site/src/contribute.ts @@ -0,0 +1,39 @@ +/** + * Contribute page: live repo stats, issues ready for newcomers, and everyone who has contributed. + * Without the API (offline, rate limited) the page keeps its plain links. + * @license AGPL-3.0 + */ + +import { REPO, gh, toContributors, toIssues } from './github'; + +const $ = (id: string) => document.getElementById(id)!; +const esc = (s: string) => s.replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]!)); +const n = (v: number) => v.toLocaleString(); + +gh<{ stargazers_count: number; forks_count: number; open_issues_count: number }>(`repos/${REPO}`).then(r => { + $('stStars').textContent = n(r.stargazers_count); + $('stForks').textContent = n(r.forks_count); + $('stIssues').textContent = n(r.open_issues_count); + $('stats').classList.add('is-live'); +}).catch(() => { /* the stat cells keep their links */ }); + +Promise.all([ + gh(`repos/${REPO}/issues?state=open&labels=good%20first%20issue&per_page=8`), + gh(`repos/${REPO}/issues?state=open&labels=help%20wanted&per_page=8`), +]).then(([first, help]) => { + const seen = new Set(); + const all = [...toIssues(first), ...toIssues(help)].filter(i => !seen.has(i.number) && seen.add(i.number)); + if (!all.length) return; + $('issues').innerHTML = all.map(i => `
        • + #${i.number}${esc(i.title)} + ${i.labels.filter(l => l !== 'enhancement' && l !== 'documentation').map(l => `${esc(l)}`).join('')} +
        • `).join(''); +}).catch(() => { /* keeps the link to the issue list */ }); + +gh(`repos/${REPO}/contributors?per_page=60`).then(raw => { + const people = toContributors(raw); + if (!people.length) return; + $('people').innerHTML = people.map(p => `
        • + ${esc(p.login)}${n(p.contributions)} commits
        • `).join('') + + `
        • You?Start below
        • `; +}).catch(() => { /* keeps the link to the contributors graph */ }); diff --git a/apps/site/src/github.test.ts b/apps/site/src/github.test.ts new file mode 100644 index 0000000..b1c80c1 --- /dev/null +++ b/apps/site/src/github.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; +import { toContributors, toIssues } from './github'; + +describe('toIssues', () => { + it('keeps issues, drops pull requests and junk', () => { + const raw = [ + { number: 22, title: 'Reduced motion', html_url: 'u', labels: [{ name: 'good first issue' }] }, + { number: 9, title: 'A PR', html_url: 'p', pull_request: {}, labels: [] }, + null, + ]; + expect(toIssues(raw)).toEqual([{ number: 22, title: 'Reduced motion', url: 'u', labels: ['good first issue'] }]); + expect(toIssues({ message: 'rate limited' })).toEqual([]); + expect(toIssues([{ number: 1, title: 'Export \u2014 add HTML', html_url: 'u', labels: [] }])[0].title).toBe('Export, add HTML'); + }); +}); + +describe('toContributors', () => { + it('keeps people and drops bots', () => { + const raw = [{ login: 'ajithakdev', type: 'User', avatar_url: 'a?v=4', html_url: 'h', contributions: 120 }, { login: 'dependabot[bot]', type: 'Bot' }]; + expect(toContributors(raw)).toEqual([{ login: 'ajithakdev', avatar: 'a?v=4&s=96', url: 'h', contributions: 120 }]); + }); +}); diff --git a/apps/site/src/github.ts b/apps/site/src/github.ts new file mode 100644 index 0000000..4836d13 --- /dev/null +++ b/apps/site/src/github.ts @@ -0,0 +1,38 @@ +/** + * Reads public data about the MeterX repo from the GitHub API, cached for the visit. + * @license AGPL-3.0 + */ + +export const REPO = 'ajithakdev/meterx'; + +export interface Issue { number: number; title: string; url: string; labels: string[] } +export interface Contributor { login: string; avatar: string; url: string; contributions: number } +export interface RepoStats { stars: number; forks: number; openIssues: number } + +/** The site never shows em or en dashes, even when an issue title has them. */ +const noDashes = (s: string) => s.replace(/\s*[\u2013\u2014]\s*/g, ', '); + +/** Open issues only: the issues endpoint also returns pull requests, which carry a pull_request key. */ +export function toIssues(raw: unknown): Issue[] { + if (!Array.isArray(raw)) return []; + return raw.filter(i => i && !i.pull_request && typeof i.number === 'number') + .map(i => ({ number: i.number, title: noDashes(String(i.title)), url: String(i.html_url), labels: (i.labels ?? []).map((l: { name: string }) => l.name) })); +} + +/** People only: bots and anonymous entries are left out. */ +export function toContributors(raw: unknown): Contributor[] { + if (!Array.isArray(raw)) return []; + return raw.filter(c => c && c.type === 'User' && typeof c.login === 'string') + .map(c => ({ login: c.login, avatar: `${c.avatar_url}&s=96`, url: c.html_url, contributions: c.contributions })); +} + +/** GET from the GitHub API, cached in sessionStorage so a visit costs one request per endpoint (the limit is 60 an hour). */ +export async function gh(path: string): Promise { + const key = `gh:${path}`; + try { const hit = sessionStorage.getItem(key); if (hit) return JSON.parse(hit) as T; } catch { /* storage blocked */ } + const res = await fetch(`https://api.github.com/${path}`, { headers: { Accept: 'application/vnd.github+json' } }); + if (!res.ok) throw new Error(`GitHub answered ${res.status}`); + const data = await res.json() as T; + try { sessionStorage.setItem(key, JSON.stringify(data)); } catch { /* storage blocked */ } + return data; +} diff --git a/apps/site/static/_headers b/apps/site/static/_headers index 407953f..646050e 100644 --- a/apps/site/static/_headers +++ b/apps/site/static/_headers @@ -2,7 +2,7 @@ X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=() - Content-Security-Policy: default-src 'self'; connect-src 'self' https://meterx-speedtest.meterx-ajithakdev.workers.dev; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; frame-ancestors 'self'; base-uri 'none'; form-action 'none' + Content-Security-Policy: default-src 'self'; connect-src 'self' https://meterx-speedtest.meterx-ajithakdev.workers.dev https://api.github.com; img-src 'self' data: https://avatars.githubusercontent.com; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; frame-ancestors 'self'; base-uri 'none'; form-action 'none' Strict-Transport-Security: max-age=31536000 /fonts/* diff --git a/apps/site/static/_partials/footer.html b/apps/site/static/_partials/footer.html index ad8b6cd..791d33b 100644 --- a/apps/site/static/_partials/footer.html +++ b/apps/site/static/_partials/footer.html @@ -6,6 +6,7 @@ Features Questions About + Contribute Privacy Source code diff --git a/apps/site/static/_partials/header.html b/apps/site/static/_partials/header.html index d799f05..9271ef8 100644 --- a/apps/site/static/_partials/header.html +++ b/apps/site/static/_partials/header.html @@ -1,7 +1,7 @@
          - MeterX + MeterX

          ZDVBxBCH`1i_ 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 diff --git a/apps/site/static/img/shot-popup-light.png b/apps/site/static/img/shot-popup-light.png deleted file mode 100644 index 3363760bc69143ffdfebfb5f6a8f05bfd039344e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105289 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 diff --git a/apps/site/static/index.html b/apps/site/static/index.html index 375d79d..b9c983c 100644 --- a/apps/site/static/index.html +++ b/apps/site/static/index.html @@ -17,139 +17,84 @@ + + - -