Repository navigation
Expand file tree
/
Copy pathCaptureScreenshots.mjs
More file actions
115 lines (103 loc) · 3.38 KB
/
Copy pathCaptureScreenshots.mjs
File metadata and controls
115 lines (103 loc) · 3.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#!/usr/bin/env node
/**
* Captures the README screenshots from a running instance.
*
* Screenshots of a UI go stale silently — the interface changes, the images do
* not, and the README quietly starts lying. Regenerating them is one command.
*
* Usage:
* pnpm build && STEWARD_DATA_MODE=demo pnpm start # in one terminal
* pnpm screenshots # in another
*
* Demo mode is required, not incidental: these images are published, and live
* mode would put real customer names and processing limits in them.
*/
import { mkdir } from "node:fs/promises";
import { chromium } from "playwright";
const BASE_URL = process.env.STEWARD_SCREENSHOT_URL ?? "http://localhost:3000";
const OUTPUT_DIR = "docs";
/** 2x for a display-quality image; 1280 keeps the sidebar layout. */
const VIEWPORT = { width: 1280, height: 860 };
const SCALE = 2;
const SHOTS = [
{
file: "screenshot-control-center.png",
path: "/",
description: "Portfolio summary and evidence coverage",
},
{
file: "screenshot-opportunity-queue.png",
path: "/",
description: "The governed action queue with its review controls",
// Scroll past the summary tiles to the queue itself.
scrollTo: "#opportunities",
},
{
file: "screenshot-account-detail.png",
path: "/accounts/acct-kilo",
description: "Account evidence, policy, and decision timeline",
},
{
file: "screenshot-signal-sources.png",
path: "/signals",
description: "The signal sources this deployment collects from",
},
];
async function launchBrowser() {
try {
return await chromium.launch();
} catch (error) {
// The bundled Chromium download is blocked on some networks. System Chrome
// renders identically for this purpose and needs no download.
process.stderr.write(
`Bundled Chromium unavailable (${error.message.split("\n")[0]}).\n` +
"Falling back to system Chrome.\n",
);
return chromium.launch({ channel: "chrome" });
}
}
async function assertDemoMode(page) {
const response = await page.request.get(`${BASE_URL}/api/steward/portfolio`);
if (!response.ok()) {
throw new Error(
`${BASE_URL}/api/steward/portfolio returned ${response.status()}. ` +
"Is the server running?",
);
}
const body = await response.json();
if (body.dataStatus !== "DEMO") {
throw new Error(
`Refusing to capture: server reports dataStatus=${body.dataStatus}. ` +
"Screenshots are published — run with STEWARD_DATA_MODE=demo.",
);
}
}
const browser = await launchBrowser();
const context = await browser.newContext({
viewport: VIEWPORT,
deviceScaleFactor: SCALE,
colorScheme: "light",
reducedMotion: "reduce",
});
const page = await context.newPage();
try {
await assertDemoMode(page);
await mkdir(OUTPUT_DIR, { recursive: true });
for (const shot of SHOTS) {
await page.goto(`${BASE_URL}${shot.path}`, { waitUntil: "networkidle" });
if (shot.scrollTo) {
await page.evaluate((selector) => {
document
.querySelector(selector)
?.scrollIntoView({ block: "start", behavior: "instant" });
}, shot.scrollTo);
await page.waitForTimeout(300);
}
const target = `${OUTPUT_DIR}/${shot.file}`;
await page.screenshot({ path: target });
process.stdout.write(`${target} — ${shot.description}\n`);
}
} finally {
await context.close();
await browser.close();
}