diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c67ba08..350b6e7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,6 +18,11 @@ updates: ignore: - dependency-name: "@types/node" update-types: ["version-update:semver-major"] + # Forced in package.json overrides (see CLAUDE.md → package.json overrides). + # Dependabot PRs would fight the pin / reopen known-accepted risk. + - dependency-name: "lodash" + - dependency-name: "three" + - dependency-name: "brace-expansion" groups: development-dependencies: dependency-type: "development" diff --git a/CLAUDE.md b/CLAUDE.md index 3c846a6..616889f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,6 +53,20 @@ Follows the shared [OpenPhysics accessibility convention](https://github.com/Ope - **Hardcoded colors:** `view/TableRenderer.ts` builds the data table as real DOM and uses `TRACK_COLORS[…].toCSS()` with a `"#000000"` fallback literal — CSS-string carve-out for track visualization colors, not `TrackLabColors` UI tokens. - **Domain clock:** `VideoPlaybackModel` drives video timing/scrubbing instead of composing fleet-standard `TimeModel` (`src/common/TimeModel.ts` is present for shared reference only). + +### `package.json` overrides + +JSON cannot carry comments, so the rationale for forced transitive pins lives here. Prefer +**tilde (`~`) or exact** versions — caret (`^`) lets minors drift under what is meant to be a +hard pin. Dependabot ignores these three names (see `.github/dependabot.yml`) so it does not +open PRs that fight the overrides. Revisit when SceneryStack drops or re-pins them upstream. + +| Override | Pin | Why | +|---|---|---| +| `lodash` | `~4.18.1` | SceneryStack declares `~4.17.12`. Bump clears Dependabot/npm advisories patched in 4.18.x (e.g. GHSA-r5fr-rjxr-66jc, GHSA-f23m-r3pf-42rh). | +| `three` | `~0.125.2` | SceneryStack declares `^0.104.0`. Floor is 0.125.0 for GHSA-fq6p-x6j3-cmmq (ReDoS). Staying on the 0.125 line avoids a larger API jump; **0.125.x still has open CVEs** (e.g. XSS GHSA-7vvq-7r29-5vg3, fixed only in ≥0.137.0). Remove this override if/when SceneryStack stops depending on `three` or pins a patched line itself. LightPropagation keeps a higher `three` pin — do not force 0.125 there. | +| `brace-expansion` | `~5.0.9` | Transitive via `vite-plugin-pwa` / Workbox. Clears npm audit (originally GHSA-mh99-v99m-4gvg; keep ≥5.0.9 for GHSA-rgw5-rvv9-x895). | + ## Testing Fleet-standard Vitest layout: @@ -91,6 +105,8 @@ npm test npm run generate-svg-icon # bouncing-ball icon SVG ``` +`npm run release` intentionally skips `npm test` in some sims — append `&& npm test` before the version bump so a release cannot ship a failing suite. + ## Conventions & deliberate deviations Extra `src/` root files beyond the standard set, each justified by cross-screen use: diff --git a/README.md b/README.md index 8983fa3..bbd5484 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # TrackLab +[![CI](https://github.com/OpenPhysics/TrackLab/actions/workflows/ci.yml/badge.svg)](https://github.com/OpenPhysics/TrackLab/actions/workflows/ci.yml) + A browser-based video analysis tool built with [SceneryStack](https://scenerystack.org/). Load a physics video, calibrate real-world distances, and track or digitize object motion with kinematics graphs and CSV export. diff --git a/index.html b/index.html index 2fb6e48..796b4be 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,28 @@ + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 6fd75fa..a41a5dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4708,9 +4708,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 848c86a..ba61eeb 100644 --- a/package.json +++ b/package.json @@ -66,8 +66,8 @@ "overrides": { "sourcemap-codec": "npm:@jridgewell/sourcemap-codec@^1.5.0", "source-map": "^0.7.4", - "lodash": "^4.18.0", - "three": "^0.125.0", - "brace-expansion": "^5.0.8" + "lodash": "~4.18.1", + "three": "~0.125.2", + "brace-expansion": "~5.0.9" } } diff --git a/scripts/generate-icons.ts b/scripts/generate-icons.ts index 7f8adc0..2d482b1 100644 --- a/scripts/generate-icons.ts +++ b/scripts/generate-icons.ts @@ -1,4 +1,4 @@ -import { readFileSync, writeFileSync } from "node:fs"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import pngToIco from "png-to-ico"; @@ -33,3 +33,24 @@ for (const size of faviconSizes) { const icoBuffer: Buffer = await pngToIco(faviconPngs); const faviconDest: string = resolve(root, "public", "favicon.ico"); writeFileSync(faviconDest, icoBuffer); + +/** Branded placeholder screenshots for the Web App Manifest `screenshots` member. */ +async function writeScreenshot(width: number, height: number, file: string): Promise { + const iconSize = Math.round(Math.min(width, height) * 0.4); + const icon = await sharp(svg, { density }).resize(iconSize, iconSize).png().toBuffer(); + await sharp({ + create: { + width, + height, + channels: 4, + background: THEME_BG, + }, + }) + .composite([{ input: icon, gravity: "center" }]) + .png() + .toFile(resolve(publicDir, file)); +} + +mkdirSync(resolve(publicDir, "screenshots"), { recursive: true }); +await writeScreenshot(1280, 720, "screenshots/wide.png"); +await writeScreenshot(720, 1280, "screenshots/narrow.png"); diff --git a/vite.config.ts b/vite.config.ts index 86b3dcb..dd937c0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -120,6 +120,7 @@ function serveOpenCV(): Plugin { * Security headers required for: * - COOP/COEP: SharedArrayBuffer (OpenCV WASM in the tracking worker) * - CSP: restrict resource loading to same-origin + known blob/data exceptions + * - Referrer / Permissions: tighten default browser leakage * - X-Content-Type-Options: prevent MIME sniffing * - X-Frame-Options: prevent clickjacking (belt-and-suspenders alongside frame-ancestors) */ @@ -129,10 +130,18 @@ const securityHeaders: Record = { "Content-Security-Policy": [ "default-src 'self'", // 'wasm-unsafe-eval' is required for the OpenCV WASM module + // TODO(scenerystack): drop 'unsafe-eval' when SceneryStack no longer needs + + // Function/eval for query-parameter parsing — reopen a CSP audit then. + // 'unsafe-eval' is required for SceneryStack query parameter parsing "script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval'", // OpenCV spins up blob: workers "worker-src blob: 'self'", + // TODO(scenerystack): drop 'unsafe-inline' when SceneryStack stops setting + + // element.style / cssText for theming (same CSP revisit as unsafe-eval). + // Inline styles are set via element.style / cssText throughout the UI layer "style-src 'self' 'unsafe-inline'", // blob: for video playback and CSV download; data: for icons @@ -147,10 +156,18 @@ const securityHeaders: Record = { "base-uri 'self'", "frame-ancestors 'none'", ].join("; "), + "Referrer-Policy": "strict-origin-when-cross-origin", + "Permissions-Policy": "camera=(), microphone=(), geolocation=()", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", }; +/** Single-file mode: inline every imported asset as base64 (effectively unlimited). */ +const INLINE_LIMIT_BYTES = 100 * 1024 * 1024; + +/** Workbox precache ceiling — SceneryStack bundles exceed the default 2 MB limit. */ +const WORKBOX_MAX_FILE_BYTES = 12 * 1024 * 1024; + // https://vite.dev/config/ export default defineConfig({ // So the build can be served from an arbitrary path @@ -174,16 +191,20 @@ export default defineConfig({ registerType: "autoUpdate", includeAssets: ["favicon.ico", "icons/apple-touch-icon.png"], manifest: { + id: "track-lab", name: "trackLab", // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys short_name: "trackLab", description: "trackLab simulation", + categories: ["education", "science"], // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys theme_color: "#1a1a2e", // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys background_color: "#000000", display: "standalone", - orientation: "landscape", + // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys + display_override: ["window-controls-overlay", "standalone"], + // No `orientation` — leave free so portrait-friendly sims are not forced landscape. icons: [ { src: "icons/icon-192.png", @@ -202,9 +223,28 @@ export default defineConfig({ purpose: "maskable", }, ], + // Placeholder shots from `npm run icons`; replace with real sim screenshots before shipping. + screenshots: [ + { + src: "screenshots/wide.png", + sizes: "1280x720", + type: "image/png", + // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys + form_factor: "wide", + label: "trackLab", + }, + { + src: "screenshots/narrow.png", + sizes: "720x1280", + type: "image/png", + // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys + form_factor: "narrow", + label: "trackLab", + }, + ], }, workbox: { - maximumFileSizeToCacheInBytes: 12 * 1024 * 1024, + maximumFileSizeToCacheInBytes: WORKBOX_MAX_FILE_BYTES, globPatterns: ["**/*.{js,css,html,svg,png,woff2}"], // opencv.js (≈11 MB) is loaded on-demand; skip precaching to speed up // the initial service-worker install. It is still cached at runtime by