All checks were successful
PR checks / checks (pull_request) Successful in 1m25s
D4 asked for screenshots of the review stack rather than placeholders. Seventeen
of them: eleven of the site in a browser, six of the app on a phone, all from one
demo deployment wired to a running ServUO shard over a real sidecar, captured on
one day (D42).
The deployment is branded "Runic Gateway Demo" rather than a real community (D43),
and the captures sit beside the claims they support — the homepage, /features/, and
five of the administration pages phase 7 could describe but not show (D44).
The rig is committed rather than remembered (D45):
scripts/seedDemo.mjs content, by driving the site's own API — never SQL,
because a row the product could not have produced is
a screenshot of a product that does not exist
src/data/screens.mjs every capture: route, viewport, scroll, alt, caption
scripts/captureScreens.mjs npm run screens:capture
scripts/checkScreens.mjs the ninth check script, in CI
Shard-side dressing is servuo-plugins' scaffolding (D46), never deployed.
The rig found five things nothing else had. One is fixed upstream — a fresh
module-uo install pinned wire protocol 3 against a sidecar speaking 4, released as
v1.0.2, which this repo's own facts check then caught in platform.json. Four are
raised as product observations and worked around in the rig: a renamed guild
member never reaches the site, a guild deleted while the shard is down is a ghost
row forever, "Houses in danger" cannot show a house that was already collapsing,
and the app's news list prints raw ISO timestamps.
Players online reads 0. Logging a character in needs a UO client driven by hand,
and that is where this stopped — PLAN.md §10 says exactly why, and how to retake
the two frames that would change.
Co-Authored-By: Claude <noreply@anthropic.com>
172 lines
6.1 KiB
JavaScript
172 lines
6.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* checkScreens.mjs — the screenshots agree with what the pages say about them.
|
|
*
|
|
* PLAN.md §12, §13 phase 9, D45.
|
|
*
|
|
* node scripts/checkScreens.mjs
|
|
*
|
|
* ---------------------------------------------------------------------------------------
|
|
* WHAT IT PROVES, AND WHY EACH ONE IS WORTH A CHECK
|
|
* ---------------------------------------------------------------------------------------
|
|
* 1. EVERY DECLARED SCREEN HAS A FILE. A missing image is invisible in review — the page
|
|
* still builds, still lays out, and only a reader sees the broken frame.
|
|
*
|
|
* 2. EVERY FILE IS THE DECLARED SIZE. `width` and `height` reach the markup as intrinsic
|
|
* attributes, and an attribute that disagrees with the file is a page that jumps as the
|
|
* image decodes. It also catches a re-capture taken at the wrong viewport, which looks
|
|
* fine on its own and wrong beside the others.
|
|
*
|
|
* 3. NOTHING IN public/screens IS ORPHANED. A capture that stopped being referenced is a
|
|
* file the container still ships and nobody looks at — and, worse, one that never gets
|
|
* retaken, so it silently becomes the oldest thing in the repository.
|
|
*
|
|
* 4. EVERY DECLARED SCREEN IS ACTUALLY USED. The mirror of 3: an entry in `screens.mjs`
|
|
* that no page renders is a capture being maintained for nothing. Usage is a literal
|
|
* search for the id across `src/`, which is how both readers of the data refer to one —
|
|
* `<Screenshot id="admin-users" />` and the `groupScreens` map on `/features/`.
|
|
*
|
|
* 5. THE ALT TEXT AND CAPTION SAY SOMETHING. An empty alt on an editorial image is an
|
|
* accessibility failure the build cannot otherwise see, and a caption is the sentence
|
|
* that makes a screenshot evidence rather than decoration.
|
|
*
|
|
* ---------------------------------------------------------------------------------------
|
|
* WHY IT READS THE PNG HEADER ITSELF
|
|
* ---------------------------------------------------------------------------------------
|
|
* It does not: it reads the WebP header, and it does it with twenty lines rather than a
|
|
* dependency. `sharp` is already here for the brand assets and could answer this, but this
|
|
* check runs in CI on every pull request and a check that needs a native image library to
|
|
* tell you a file is 1920 pixels wide is a check that will one day fail for a reason that
|
|
* has nothing to do with screenshots.
|
|
*/
|
|
|
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { screens, WEB, PHONE } from '../src/data/screens.mjs';
|
|
|
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.join(HERE, '..');
|
|
const DIR = path.join(ROOT, 'public', 'screens');
|
|
const SRC = path.join(ROOT, 'src');
|
|
|
|
const problems = [];
|
|
|
|
/**
|
|
* The pixel size of a WebP file, from its header.
|
|
*
|
|
* A RIFF container: "RIFF" size "WEBP" then one of three chunk types. Lossy ("VP8 ") and
|
|
* lossless ("VP8L") pack the dimensions differently, and an animated or extended file
|
|
* ("VP8X") states them outright. `cwebp` at quality 82 writes VP8 , but a future change of
|
|
* encoder should not turn this check into a mystery, so all three are handled.
|
|
*/
|
|
function webpSize(file) {
|
|
const buf = readFileSync(file);
|
|
|
|
if (buf.length < 30 || buf.toString('ascii', 0, 4) !== 'RIFF' || buf.toString('ascii', 8, 12) !== 'WEBP') {
|
|
return null;
|
|
}
|
|
|
|
const chunk = buf.toString('ascii', 12, 16);
|
|
|
|
if (chunk === 'VP8X') {
|
|
return {
|
|
width: 1 + (buf[24] | (buf[25] << 8) | (buf[26] << 16)),
|
|
height: 1 + (buf[27] | (buf[28] << 8) | (buf[29] << 16)),
|
|
};
|
|
}
|
|
|
|
if (chunk === 'VP8L') {
|
|
const bits = buf[21] | (buf[22] << 8) | (buf[23] << 16) | (buf[24] << 24);
|
|
return { width: 1 + (bits & 0x3fff), height: 1 + ((bits >> 14) & 0x3fff) };
|
|
}
|
|
|
|
if (chunk === 'VP8 ') {
|
|
return {
|
|
width: buf.readUInt16LE(26) & 0x3fff,
|
|
height: buf.readUInt16LE(28) & 0x3fff,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Every file under `src/`, read once, so usage is a search rather than a guess. */
|
|
function sourceText() {
|
|
const out = [];
|
|
|
|
const walk = (dir) => {
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) walk(full);
|
|
else if (/\.(astro|mdx?|mjs|js|ts|tsx)$/.test(entry.name)) out.push(readFileSync(full, 'utf8'));
|
|
}
|
|
};
|
|
|
|
walk(SRC);
|
|
return out;
|
|
}
|
|
|
|
const sources = sourceText();
|
|
const declared = new Set();
|
|
|
|
for (const shot of screens) {
|
|
const name = `${shot.id}.webp`;
|
|
const file = path.join(DIR, name);
|
|
declared.add(name);
|
|
|
|
if (!existsSync(file)) {
|
|
problems.push(
|
|
`${shot.id}: no file at public/screens/${name}. ` +
|
|
`Retake it: node scripts/captureScreens.mjs ${shot.id}`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const want = shot.family === 'web' ? WEB : PHONE;
|
|
const size = webpSize(file);
|
|
|
|
if (!size) {
|
|
problems.push(`${shot.id}: public/screens/${name} is not a WebP this check can read.`);
|
|
} else if (size.width !== want.width || size.height !== want.height) {
|
|
problems.push(
|
|
`${shot.id}: file is ${size.width}x${size.height}, ` +
|
|
`declared ${want.width}x${want.height} for the "${shot.family}" family.`,
|
|
);
|
|
}
|
|
|
|
if (!shot.alt || shot.alt.length < 20) {
|
|
problems.push(`${shot.id}: alt text is missing or too short to describe the screen.`);
|
|
}
|
|
|
|
if (!shot.caption) {
|
|
problems.push(`${shot.id}: no caption.`);
|
|
}
|
|
|
|
const used = sources.some((text) => text.includes(`'${shot.id}'`) || text.includes(`"${shot.id}"`));
|
|
|
|
if (!used) {
|
|
problems.push(
|
|
`${shot.id}: declared but no page renders it. Use it, or delete the entry and its file.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (existsSync(DIR)) {
|
|
for (const name of readdirSync(DIR)) {
|
|
if (!declared.has(name)) {
|
|
problems.push(`public/screens/${name}: not declared in src/data/screens.mjs.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (problems.length > 0) {
|
|
console.error(`\ncheckScreens: ${problems.length} problem(s)\n`);
|
|
for (const problem of problems) console.error(` - ${problem}`);
|
|
console.error('');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`checkScreens: ${screens.length} screens, all present, sized and used.`);
|