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
+[](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