feat: add gear provenance models and browser fetcher

This commit is contained in:
ik
2026-09-20 15:50:43 +07:00
parent b0edae98c6
commit 4e9895fbf4
19 changed files with 986 additions and 20 deletions
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env node
import fs from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { chromium } from "../apps/web/node_modules/playwright/index.mjs";
const routes = [
"/", "/waterbodies", "/records", "/report", "/status", "/media", "/rules",
"/waterbodies/р-вьюнок", "/fish/pike", "/admin", "/admin/moderation",
"/admin/external-sources", "/admin/media",
];
const viewports = [
[320, 800], [390, 844], [768, 900], [1280, 900],
];
const themes = ["system", "light", "dark"];
function usage() {
console.log("Usage: node scripts/capture-visual-matrix.mjs [--base-url URL] [--output DIR]");
}
function parseArgs(argv) {
const args = { baseUrl: process.env.WEB_URL || "http://127.0.0.1:4321", output: "/tmp/rf4-visual-matrix" };
for (let index = 0; index < argv.length; index += 1) {
if (argv[index] === "--help" || argv[index] === "-h") {
usage();
process.exit(0);
}
if (argv[index] === "--base-url") args.baseUrl = argv[++index];
else if (argv[index] === "--output") args.output = argv[++index];
else throw new Error(`unknown argument: ${argv[index]}`);
}
return args;
}
function safeName(value) {
return value.replace(/^\//, "home").replaceAll("/", "-").replace(/[^\p{L}\p{N}._-]+/gu, "-");
}
const { baseUrl, output } = parseArgs(process.argv.slice(2));
const outputDir = path.resolve(output);
await fs.mkdir(outputDir, { recursive: true });
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const manifest = [];
try {
for (const theme of themes) {
await page.context().clearCookies();
if (theme !== "system") {
await page.context().addCookies([{ name: "rf4-theme", value: theme, url: baseUrl }]);
}
await page.emulateMedia({ colorScheme: theme === "system" ? "light" : theme });
for (const [width, height] of viewports) {
await page.setViewportSize({ width, height });
for (const route of routes) {
await page.goto(new URL(route, baseUrl).toString(), { waitUntil: "networkidle" });
const state = await page.evaluate(() => ({
clientWidth: document.documentElement.clientWidth,
scrollWidth: document.documentElement.scrollWidth,
theme: document.documentElement.dataset.theme || "system",
}));
const filename = `${theme}-${width}x${height}-${safeName(route)}.png`;
await page.screenshot({ path: path.join(outputDir, filename), fullPage: true });
manifest.push({ theme, width, height, route, filename, ...state });
}
}
}
} finally {
await browser.close();
}
await fs.writeFile(
path.join(outputDir, "manifest.json"),
`${JSON.stringify({ baseUrl, count: manifest.length, items: manifest }, null, 2)}\n`,
"utf8",
);
const overflow = manifest.filter((item) => item.scrollWidth > item.clientWidth);
console.log(`captured ${manifest.length} screenshots in ${outputDir}`);
console.log(`document overflow: ${overflow.length}`);
if (overflow.length) process.exitCode = 1;