Credibility pass: the session seams sanded from the clips-and-camera batch
Two blind reviews of everything since the last pass (afa0e17), every
finding checked against the code, no behavior changed: all thirty
scene goldens match without a re-bless and the engine suite is
untouched.
Reel and renderer: the camera's look-down rule is stated once, beside
LOOK_DOWN, instead of twice in the effect; the empty aim branch that
stood where a cutaway used to be is gone (the guard it implied is now
explicit); the pit events and the punch are handled by their own
types, not through "in" casts; smoothstep is one export used by every
tween instead of eleven inline copies; the two floor rings share one
painter; project() takes a Billboard instead of a third hand-typed
copy of its fields; the strides-left figure and the web rim no longer
shadow the reel's steps and the pane's fx; the die card's verdict is
built from events, not by matching an emoji; the workshop asks for the
hover cue by name instead of passing an empty click handler.
Server and engine: one requestBase() for the origin, one slug pattern
in store.ts gating both the clip page and its files, one 404 for both;
LOOPBACK sits above its only caller; doCounteract names what a counter
is played against once; fearCells sits beside its own docblock rather
than between sightedCellsFor and its.
Deploy: chromiumExe, the private server, ffmpeg, and the reel rewind
live in deploy/lib/harness.mjs, shared by the gate, the recorder, and
the card cutter instead of pasted three times; the recorder drops its
duplicate frame counters and names its poster settle; the card uses the
gallery's exact gold; the one-time Sentry URL bootstrap leaves
deploy.sh; the backup comment states the rule rather than the incident.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
ddd2e347bd
commit
e1a740119c
+7
-21
@@ -14,9 +14,10 @@
|
||||
// Needs ffmpeg + ffprobe and the Playwright chromium cache (see
|
||||
// verify-scenes.mjs). Idempotent: run it again after re-recording.
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { chromium } from "playwright-core";
|
||||
import { chromiumExe, ff } from "./lib/harness.mjs";
|
||||
|
||||
const dir = process.argv[2];
|
||||
if (!dir || !existsSync(join(dir, "clips.json"))) {
|
||||
@@ -29,22 +30,6 @@ if (!dir || !existsSync(join(dir, "clips.json"))) {
|
||||
const VIEWPORT = { x: 18, y: 51, w: 700, h: 393 };
|
||||
const CARD = { width: 1200, height: 630 };
|
||||
|
||||
function chromiumExe() {
|
||||
if (process.env.WIZWAR_CHROME) return process.env.WIZWAR_CHROME;
|
||||
const cache = join(process.env.HOME ?? "", "Library", "Caches", "ms-playwright");
|
||||
const builds = existsSync(cache)
|
||||
? readdirSync(cache).filter((d) => /^chromium-\d+$/.test(d)).sort()
|
||||
: [];
|
||||
for (const build of builds.reverse()) {
|
||||
const exe = join(cache, build, "chrome-mac-arm64",
|
||||
"Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing");
|
||||
if (existsSync(exe)) return exe;
|
||||
}
|
||||
throw new Error("no Playwright chromium found — run: npx playwright-core install chromium (or set WIZWAR_CHROME)");
|
||||
}
|
||||
|
||||
const ff = (...args) => execFileSync("ffmpeg", ["-loglevel", "error", "-y", ...args]);
|
||||
|
||||
function faststart(file) {
|
||||
const tmp = `${file}.tmp.mp4`;
|
||||
ff("-i", file, "-c", "copy", "-movflags", "+faststart", tmp);
|
||||
@@ -61,10 +46,11 @@ const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<");
|
||||
|
||||
/** The share card: the viewport scaled to the card's width (a 16:9 crop
|
||||
* runs 674 tall at 1200 wide, so 22px shave top and bottom), a shade
|
||||
* rising from the bottom, the title in the gallery's gold over it. */
|
||||
* rising from the bottom, the title over it in the gallery's gold — set
|
||||
* in Georgia, since the card is rendered offline with no web fonts. */
|
||||
function cardHtml(title, viewportPngDataUrl) {
|
||||
return `<!doctype html><html><head><meta charset="utf-8"><style>
|
||||
html, body { margin: 0; width: ${CARD.width}px; height: ${CARD.height}px; overflow: hidden; background: #171a21; }
|
||||
html, body { margin: 0; width: ${CARD.width}px; height: ${CARD.height}px; overflow: hidden; background: #171a20; }
|
||||
.card { position: relative; width: ${CARD.width}px; height: ${CARD.height}px;
|
||||
font-family: Georgia, "Times New Roman", serif; color: #d6d2c4; }
|
||||
.card img { position: absolute; left: 0; top: -22px; width: ${CARD.width}px; height: 674px;
|
||||
@@ -72,11 +58,11 @@ function cardHtml(title, viewportPngDataUrl) {
|
||||
.shade { position: absolute; inset: 0;
|
||||
background: linear-gradient(to bottom, rgba(23,26,33,0) 45%, rgba(23,26,33,0.92) 100%); }
|
||||
.title { position: absolute; left: 56px; right: 56px; bottom: 94px;
|
||||
font-size: 66px; line-height: 1.1; color: #e8c87a; letter-spacing: 0.02em;
|
||||
font-size: 66px; line-height: 1.1; color: #e0b34a; letter-spacing: 0.02em;
|
||||
text-shadow: 0 2px 12px rgba(0,0,0,0.6); }
|
||||
.mark { position: absolute; left: 58px; bottom: 52px; font-size: 26px; letter-spacing: 0.08em;
|
||||
color: #d6d2c4; opacity: 0.9; text-shadow: 0 1px 8px rgba(0,0,0,0.6); }
|
||||
.mark b { font-weight: normal; color: #e8c87a; }
|
||||
.mark b { font-weight: normal; color: #e0b34a; }
|
||||
</style></head><body><div class="card">
|
||||
<img src="${viewportPngDataUrl}">
|
||||
<div class="shade"></div>
|
||||
|
||||
+2
-9
@@ -11,18 +11,11 @@ ssh "root@$HOST" '
|
||||
cd /opt/wizwar && npm install --no-audit --no-fund
|
||||
chown -R wizwar:wizwar /opt/wizwar
|
||||
cp /opt/wizwar/deploy/wizwar.service /etc/systemd/system/wizwar.service
|
||||
# The backup script is cron-run from /usr/local/bin — install it every
|
||||
# deploy, or the repo copy and the live copy drift apart (they did:
|
||||
# the Sentry check-ins shipped in the repo and never reached the box).
|
||||
# Cron runs the backup and the rollup from /usr/local/bin: install
|
||||
# them on every deploy so the live copies are always the repo copies.
|
||||
install -m 755 /opt/wizwar/deploy/wizwar-backup.sh /usr/local/bin/wizwar-backup.sh
|
||||
# The nightly rollup and its cron entry ride along the same way; its
|
||||
# Sentry check-in URL is the backup monitor'"'"'s with its own slug.
|
||||
install -m 755 /opt/wizwar/deploy/wizwar-rollup.sh /usr/local/bin/wizwar-rollup.sh
|
||||
install -m 644 /opt/wizwar/deploy/wizwar-rollup.cron /etc/cron.d/wizwar-rollup
|
||||
if [ -f /root/.wizwar-sentry-cron ] && [ ! -f /root/.wizwar-sentry-cron-rollup ]; then
|
||||
sed "s#/cron/new-monitor/#/cron/wizwar-rollup/#" /root/.wizwar-sentry-cron > /root/.wizwar-sentry-cron-rollup
|
||||
chmod 600 /root/.wizwar-sentry-cron-rollup
|
||||
fi
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now wizwar
|
||||
systemctl restart wizwar
|
||||
|
||||
+19
-49
@@ -10,50 +10,25 @@
|
||||
//
|
||||
// Run from the repo root AFTER `npx vite build` in packages/web. Needs
|
||||
// ffmpeg and the Playwright chromium cache (see verify-scenes.mjs).
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { chromium } from "playwright-core";
|
||||
import { chromiumExe, ff, rewindReel, startServer } from "./lib/harness.mjs";
|
||||
import { SCREENPLAYS } from "../packages/web/src/fpv/screenplays.ts";
|
||||
|
||||
const [outDir, ...only] = process.argv.slice(2);
|
||||
if (!outDir) { console.error("usage: film-clips.mjs <out-dir> [scene ...]"); process.exit(1); }
|
||||
const PORT = 8809;
|
||||
const FPS = 25;
|
||||
const TAIL_MS = 2500; // hold on the last move
|
||||
const TAIL_MS = 2500; // hold on the last move
|
||||
const POSTER_SETTLE_MS = 1200; // the poster beat's tween and fx, done
|
||||
const EPOCH = new Date("2026-01-01T00:00:00Z").getTime();
|
||||
|
||||
function chromiumExe() {
|
||||
if (process.env.WIZWAR_CHROME) return process.env.WIZWAR_CHROME;
|
||||
const cache = join(process.env.HOME ?? "", "Library", "Caches", "ms-playwright");
|
||||
const builds = existsSync(cache)
|
||||
? readdirSync(cache).filter((d) => /^chromium-\d+$/.test(d)).sort()
|
||||
: [];
|
||||
for (const build of builds.reverse()) {
|
||||
const exe = join(cache, build, "chrome-mac-arm64",
|
||||
"Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing");
|
||||
if (existsSync(exe)) return exe;
|
||||
}
|
||||
throw new Error("no Playwright chromium found — run: npx playwright-core install chromium (or set WIZWAR_CHROME)");
|
||||
}
|
||||
|
||||
const dataDir = mkdtempSync(join(tmpdir(), "wizwar-film-"));
|
||||
const server = spawn("npx", ["tsx", "src/index.ts"], {
|
||||
cwd: join(import.meta.dirname, "..", "packages", "server"),
|
||||
env: { ...process.env, PORT: String(PORT), WIZWAR_DATA_DIR: join(dataDir, "rooms") },
|
||||
stdio: "ignore",
|
||||
});
|
||||
const up = async () => {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try { if ((await fetch(`http://127.0.0.1:${PORT}/`)).ok) return; } catch { /* not yet */ }
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
throw new Error("server never came up");
|
||||
};
|
||||
const server = startServer(PORT, "wizwar-film");
|
||||
|
||||
try {
|
||||
await up();
|
||||
await server.up();
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const browser = await chromium.launch({
|
||||
executablePath: chromiumExe(),
|
||||
@@ -71,9 +46,7 @@ try {
|
||||
const page = await ctx.newPage();
|
||||
await page.clock.install({ time: EPOCH });
|
||||
await page.goto(`http://127.0.0.1:${PORT}/?fpv&script=${sp.name}`, { waitUntil: "networkidle" });
|
||||
await page.locator("button", { hasText: "❚❚" }).click();
|
||||
const count = Number((await page.locator(".replay-count").textContent()).match(/of (\d+)/)[1]);
|
||||
for (let i = 0; i < count; i++) await page.locator("button", { hasText: "◀" }).click();
|
||||
const count = await rewindReel(page);
|
||||
if (view === "fpv") await page.getByText("your eyes").click();
|
||||
await page.clock.pauseAt(EPOCH + 60_000);
|
||||
await page.clock.runFor(400); // let the first frame settle before rolling
|
||||
@@ -82,36 +55,34 @@ try {
|
||||
// Roll: the reel's play button is the ▶ before the step button.
|
||||
await page.locator("button", { hasText: "▶" }).first().click();
|
||||
// Roll until the reel reaches its last move (steps a die decides
|
||||
// run longer than the beat), then hold the tail. The poster is
|
||||
// the first pinned beat, settled.
|
||||
// run longer than the beat), then hold the tail.
|
||||
const posterBeat = sp.beats?.[0] ?? 1;
|
||||
let f = 0, tail = -1, posterIn = -1, total = 0;
|
||||
const MAX_FRAMES = FPS * 240; // a reel that never ends is a bug, not a feature
|
||||
const MAX_FRAMES = FPS * 240; // four minutes: a reel that never reaches its last move
|
||||
let f = 0, tail = -1, posterIn = -1;
|
||||
while (tail !== 0 && f < MAX_FRAMES) {
|
||||
const png = await page.screenshot({ clip });
|
||||
writeFileSync(join(frames, `${String(f).padStart(5, "0")}.png`), png);
|
||||
const at = Number((await page.locator(".replay-count").textContent()).match(/move (\d+)/)[1]);
|
||||
if (view === "fpv" && posterIn < 0 && at === posterBeat + 1) posterIn = Math.round(1200 / (1000 / FPS));
|
||||
if (view === "fpv" && posterIn < 0 && at === posterBeat + 1) posterIn = Math.round(POSTER_SETTLE_MS / (1000 / FPS));
|
||||
if (posterIn === 0) writeFileSync(join(frames, "poster.png"), png);
|
||||
if (posterIn >= 0) posterIn--;
|
||||
if (tail < 0 && at === count) tail = Math.round(TAIL_MS / (1000 / FPS));
|
||||
if (tail > 0) tail--;
|
||||
await page.clock.runFor(1000 / FPS);
|
||||
f++; total += 1000 / FPS;
|
||||
f++;
|
||||
}
|
||||
const nFrames = f;
|
||||
await ctx.close();
|
||||
execFileSync("ffmpeg", ["-loglevel", "error", "-y", "-framerate", String(FPS),
|
||||
"-i", join(frames, "%05d.png"), "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", // h264 wants even sides
|
||||
ff("-framerate", String(FPS), "-i", join(frames, "%05d.png"),
|
||||
"-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", // h264 wants even sides
|
||||
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-crf", "22",
|
||||
"-movflags", "+faststart", join(outDir, `${sp.name}-${view}.mp4`)]);
|
||||
"-movflags", "+faststart", join(outDir, `${sp.name}-${view}.mp4`));
|
||||
if (view === "fpv") {
|
||||
const poster = existsSync(join(frames, "poster.png")) ? join(frames, "poster.png") : join(frames, "00000.png");
|
||||
execFileSync("ffmpeg", ["-loglevel", "error", "-y", "-i", poster, "-q:v", "3", join(outDir, `${sp.name}.jpg`)]);
|
||||
seconds = Math.round(total / 1000);
|
||||
ff("-i", poster, "-q:v", "3", join(outDir, `${sp.name}.jpg`));
|
||||
seconds = Math.round(f / FPS);
|
||||
}
|
||||
rmSync(frames, { recursive: true, force: true });
|
||||
console.log(`${sp.name} ${view}: ${nFrames} frames`);
|
||||
console.log(`${sp.name} ${view}: ${f} frames`);
|
||||
}
|
||||
catalog.push({ name: sp.name, title: sp.title, blurb: sp.blurb, seconds });
|
||||
}
|
||||
@@ -124,6 +95,5 @@ try {
|
||||
writeFileSync(file, JSON.stringify(merged, null, 2) + "\n");
|
||||
console.log(`filmed ${catalog.length} scenes into ${outDir}`);
|
||||
} finally {
|
||||
server.kill();
|
||||
rmSync(dataDir, { recursive: true, force: true });
|
||||
server.stop();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Shared by the scene gate, the clip recorder, and the card cutter: the
|
||||
// Playwright chromium they all drive, a private game server on a
|
||||
// throwaway data dir, ffmpeg with the flags they all want, and the
|
||||
// reel's rewind. Run from the repo root after `npx vite build` in
|
||||
// packages/web.
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
/** The Playwright chromium executable: $WIZWAR_CHROME, else the newest
|
||||
* build in the ms-playwright cache. */
|
||||
export function chromiumExe() {
|
||||
if (process.env.WIZWAR_CHROME) return process.env.WIZWAR_CHROME;
|
||||
const cache = join(process.env.HOME ?? "", "Library", "Caches", "ms-playwright");
|
||||
const builds = existsSync(cache)
|
||||
? readdirSync(cache).filter((d) => /^chromium-\d+$/.test(d)).sort()
|
||||
: [];
|
||||
for (const build of builds.reverse()) {
|
||||
const exe = join(cache, build, "chrome-mac-arm64",
|
||||
"Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing");
|
||||
if (existsSync(exe)) return exe;
|
||||
}
|
||||
throw new Error("no Playwright chromium found — run: npx playwright-core install chromium (or set WIZWAR_CHROME)");
|
||||
}
|
||||
|
||||
/** A game server on `port` with its own empty data dir. `up()` resolves
|
||||
* once it answers; `stop()` kills it and removes the dir. */
|
||||
export function startServer(port, label = "wizwar-harness") {
|
||||
const dataDir = mkdtempSync(join(tmpdir(), `${label}-`));
|
||||
const server = spawn("npx", ["tsx", "src/index.ts"], {
|
||||
cwd: join(import.meta.dirname, "..", "..", "packages", "server"),
|
||||
env: { ...process.env, PORT: String(port), WIZWAR_DATA_DIR: join(dataDir, "rooms") },
|
||||
stdio: "ignore",
|
||||
});
|
||||
const up = async () => {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try { if ((await fetch(`http://127.0.0.1:${port}/`)).ok) return; } catch { /* not yet */ }
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
throw new Error("server never came up");
|
||||
};
|
||||
const stop = () => { server.kill(); rmSync(dataDir, { recursive: true, force: true }); };
|
||||
return { up, stop };
|
||||
}
|
||||
|
||||
/** ffmpeg, quiet and overwriting. */
|
||||
export const ff = (...args) => execFileSync("ffmpeg", ["-loglevel", "error", "-y", ...args]);
|
||||
|
||||
/** Pause the reel and step it back to its first move. */
|
||||
export async function rewindReel(page) {
|
||||
await page.locator("button", { hasText: "❚❚" }).click();
|
||||
const count = Number((await page.locator(".replay-count").textContent()).match(/of (\d+)/)[1]);
|
||||
for (let i = 0; i < count; i++) await page.locator("button", { hasText: "◀" }).click();
|
||||
return count;
|
||||
}
|
||||
@@ -25,8 +25,8 @@ mkdir -p /opt/wizwar /var/lib/wizwar/rooms
|
||||
chown -R wizwar:wizwar /opt/wizwar /var/lib/wizwar
|
||||
|
||||
# Caddy vhost: auto-TLS, security headers, proxy to the game.
|
||||
# Access log kept ~30 days (10MiB x 30): the nightly rollup keeps the
|
||||
# counts forever, the raw lines back it for a month.
|
||||
# Access log kept to 30 rolls of 10MiB: the nightly rollup keeps the
|
||||
# counts; the raw lines back it for a while.
|
||||
printf '%s\n\nheader {\n\tStrict-Transport-Security "max-age=31536000"\n\tX-Content-Type-Options "nosniff"\n\tX-Frame-Options "DENY"\n\tReferrer-Policy "no-referrer"\n}\nlog {\n\toutput file /var/lib/caddy/access.log {\n\t\troll_size 10MiB\n\t\troll_keep 30\n\t}\n}\nreverse_proxy localhost:8787\n' "$HOST" > /etc/caddy/Caddyfile
|
||||
systemctl reload caddy
|
||||
|
||||
|
||||
+12
-45
@@ -11,11 +11,12 @@
|
||||
// Run from the repo root AFTER `npx vite build` in packages/web (the
|
||||
// wrapper deploy/verify-scenes.sh does both). Needs the Playwright
|
||||
// chromium cache (`npx playwright-core install chromium` if missing).
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { chromium } from "playwright-core";
|
||||
import { chromiumExe, rewindReel, startServer } from "./lib/harness.mjs";
|
||||
import { SCREENPLAYS } from "../packages/web/src/fpv/screenplays.ts";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
@@ -31,20 +32,6 @@ const CHANNEL_TOL = 6;
|
||||
const FAIL_FRAC = 0.002;
|
||||
const EPOCH = new Date("2026-01-01T00:00:00Z").getTime();
|
||||
|
||||
function chromiumExe() {
|
||||
if (process.env.WIZWAR_CHROME) return process.env.WIZWAR_CHROME;
|
||||
const cache = join(process.env.HOME ?? "", "Library", "Caches", "ms-playwright");
|
||||
const builds = existsSync(cache)
|
||||
? readdirSync(cache).filter((d) => /^chromium-\d+$/.test(d)).sort()
|
||||
: [];
|
||||
for (const build of builds.reverse()) {
|
||||
const exe = join(cache, build, "chrome-mac-arm64",
|
||||
"Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing");
|
||||
if (existsSync(exe)) return exe;
|
||||
}
|
||||
throw new Error("no Playwright chromium found — run: npx playwright-core install chromium (or set WIZWAR_CHROME)");
|
||||
}
|
||||
|
||||
/** Decode a PNG to raw RGBA via ffmpeg — the harness's only image dep. */
|
||||
function rgba(png) {
|
||||
return execFileSync("ffmpeg", ["-loglevel", "error", "-i", png,
|
||||
@@ -64,26 +51,11 @@ function compare(goldenPng, actualPng) {
|
||||
}
|
||||
|
||||
// --- A private server on a throwaway data dir. ---------------------------
|
||||
const dataDir = mkdtempSync(join(tmpdir(), "wizwar-scenes-"));
|
||||
const server = spawn("npx", ["tsx", "src/index.ts"], {
|
||||
cwd: join(import.meta.dirname, "..", "packages", "server"),
|
||||
env: { ...process.env, PORT: String(PORT), WIZWAR_DATA_DIR: join(dataDir, "rooms") },
|
||||
stdio: "ignore",
|
||||
});
|
||||
const up = async () => {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try {
|
||||
const r = await fetch(`http://127.0.0.1:${PORT}/`);
|
||||
if (r.ok) return;
|
||||
} catch { /* not yet */ }
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
throw new Error("server never came up");
|
||||
};
|
||||
const server = startServer(PORT, "wizwar-scenes");
|
||||
|
||||
let failures = 0;
|
||||
try {
|
||||
await up();
|
||||
await server.up();
|
||||
mkdirSync(GOLDENS, { recursive: true });
|
||||
rmSync(DIFFS, { recursive: true, force: true });
|
||||
mkdirSync(DIFFS, { recursive: true });
|
||||
@@ -95,10 +67,8 @@ try {
|
||||
if (!scenes.length) throw new Error(`no scenes match: ${only.join(", ")}`);
|
||||
|
||||
for (const sp of scenes) {
|
||||
// One page per view: the reel steps forward beat to beat under the
|
||||
// paused clock, so a scene's later beats cost only their marginal
|
||||
// steps — and every beat's simulated capture time is still exactly
|
||||
// (steps so far) × 2.6s, identical every run.
|
||||
// One page per view: the reel steps forward beat to beat, so a
|
||||
// scene's later beats cost only their marginal steps.
|
||||
const takes = [
|
||||
{ view: "fpv", beats: [...(sp.beats ?? [])].sort((a, b) => a - b) },
|
||||
{ view: "board", beats: sp.boardBeat === undefined ? [] : [sp.boardBeat] },
|
||||
@@ -109,13 +79,11 @@ try {
|
||||
const page = await ctx.newPage();
|
||||
await page.clock.install({ time: EPOCH });
|
||||
await page.goto(`http://127.0.0.1:${PORT}/?fpv&script=${sp.name}`, { waitUntil: "networkidle" });
|
||||
await page.locator("button", { hasText: "❚❚" }).click();
|
||||
for (let i = 0; i < 4; i++) await page.locator("button", { hasText: "◀" }).click();
|
||||
await rewindReel(page);
|
||||
if (view === "fpv") await page.getByText("your eyes").click();
|
||||
// Freeze the world at a fixed instant, then advance it by hand.
|
||||
// Steps on the way to a beat get 0.6 simulated seconds; the beat
|
||||
// itself gets 2.6 to settle its tween, hold, and fx. The schedule
|
||||
// is fixed, so every run renders the same bytes.
|
||||
// Freeze the world at a fixed instant, then advance it by hand:
|
||||
// steps on the way to a beat get 0.6 simulated seconds, the beat
|
||||
// itself 2.6 to settle its tween, hold, and fx.
|
||||
await page.clock.pauseAt(EPOCH + 60_000);
|
||||
let at = 0;
|
||||
for (const beat of beats) {
|
||||
@@ -154,8 +122,7 @@ try {
|
||||
}
|
||||
await browser.close();
|
||||
} finally {
|
||||
server.kill();
|
||||
rmSync(dataDir, { recursive: true, force: true });
|
||||
server.stop();
|
||||
}
|
||||
if (!update) console.log(`--- ${failures ? `${failures} beat(s) drifted` : "every pinned beat matches its golden"}`);
|
||||
process.exit(failures ? 1 : 0);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# self-rotating access log cannot keep.
|
||||
# Cron: 10 0 * * * (UTC), see /etc/cron.d/wizwar-rollup (deploy.sh installs both).
|
||||
# Sentry Crons check-in: /root/.wizwar-sentry-cron-rollup holds the URL
|
||||
# (derived from the backup monitor's on first deploy; absent = no check-ins).
|
||||
# (absent = no check-ins).
|
||||
set -u
|
||||
OUT="/var/lib/wizwar/rollup.jsonl"
|
||||
LOG="/var/log/wizwar/rollup.log"
|
||||
@@ -89,8 +89,9 @@ def sh(cmd):
|
||||
mem = sh("systemctl show wizwar -p MemoryCurrent --value"); peak = sh("systemctl show wizwar -p MemoryPeak --value")
|
||||
disk = sh("df --output=pcent / | tail -1").strip().rstrip("%")
|
||||
load = sh("cut -d' ' -f1 /proc/loadavg")
|
||||
errs = sh("journalctl -u wizwar --since '%s' --until '%s' --no-pager 2>/dev/null | grep -ci 'unhandled protocol error'" % (day, (d0 + datetime.timedelta(days=1)).date()))
|
||||
starts = sh("journalctl -u wizwar --since '%s' --until '%s' --no-pager 2>/dev/null | grep -c 'Started wizwar'" % (day, (d0 + datetime.timedelta(days=1)).date()))
|
||||
journal = sh("journalctl -u wizwar --since '%s' --until '%s' --no-pager 2>/dev/null" % (day, (d0 + datetime.timedelta(days=1)).date()))
|
||||
errs = str(len(re.findall(r"(?i)unhandled protocol error", journal)))
|
||||
starts = str(journal.count("Started wizwar"))
|
||||
ledger = sum(os.path.getsize(f) for f in glob.glob("/var/lib/wizwar/rooms/*.jsonl"))
|
||||
row = {
|
||||
"day": day, "requests": req, "human": human, "bot": bot,
|
||||
|
||||
Reference in New Issue
Block a user