docs: verify against real WHP + capture real screenshots

Discovery against the demo account on whp01 surfaced several inaccuracies:

- Cache is Valkey (Redis wire-compatible), not Redis or Memcached.
  No Memcached is offered as a separate service.
- Site Monitoring is the sidebar label (not 'AI Monitor').
- 'Add a domain' has no Primary/Add-on distinction.
- Sites form: 'Container Type' (not 'Site type'), Number of Containers
  (1-10 for horizontal scaling), CPU per Container (default 0.25),
  Memory per Container (default 256MB), SSL inline on the same form.
- Backups: default retention 5 days / 10 backups; on-demand + scheduled;
  S3 backup targets are visible and configurable.
- Email: per-domain settings live behind 'Setup Instructions' on the
  Email page; mail server hostname is on the Dashboard (per-server,
  e.g. mail01.cloud-hosting.io), not per-domain.

Also reworked the screenshot pipeline:
- New shots.config.ts targets the real index.php?page=... URLs
- Added redactSensitive() step that runs before each screenshot to swap
  server names, IPs, mail hostnames, and demo-user-isms with neutral
  placeholders. This keeps docs portable across the fleet.
- Hides .brand-full and .navbar-text (top-bar server identifier and
  Welcome greeting).
- Captured 9 real WHP screenshots; removed stale placeholders.
This commit is contained in:
2026-05-17 17:00:13 -07:00
parent 53bc37fd0d
commit c602b8f8f3
32 changed files with 460 additions and 152 deletions

View File

@@ -22,13 +22,57 @@ const WHP_USER = envOrDie('WHP_USER');
const WHP_PASS = envOrDie('WHP_PASS');
async function login(page: Page): Promise<void> {
await page.goto(`${WHP_BASE}/login`);
await page.fill('input[name="username"]', WHP_USER);
await page.goto(`${WHP_BASE}/login.php`);
await page.fill('input[name="user"]', WHP_USER);
await page.fill('input[name="password"]', WHP_PASS);
await page.click('button[type="submit"]');
await page.waitForLoadState('networkidle');
}
/**
* Neutralise server-identifying and account-specific text/labels before the
* screenshot — we run a multi-server fleet, so docs shouldn't bake in a single
* host's name or IP.
*
* Runs in the page context. Self-contained: no external bindings, no eval.
*/
async function redactSensitive(page: Page): Promise<void> {
await page.addStyleTag({
content: `
.navbar-text { visibility: hidden !important; }
.brand-full { visibility: hidden !important; }
`,
});
// String swaps for inline text that mask-by-selector can't cover.
await page.evaluate(() => {
const root = document.body;
const swaps: [RegExp, string][] = [
// server / mail / nameserver hostnames (with possible -s3 etc. suffix)
[/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '<your-server>.cloud-hosting.io'],
[/mail\d+\.cloud-hosting\.io/gi, '<your-mail-server>.cloud-hosting.io'],
[/ns[12]\.whp\d+\.cloud-hosting\.io/gi, 'ns<n>.your-server.cloud-hosting.io'],
// backup target / bucket names that bake in server number
[/whp\d+(-[a-z0-9]+)?\b/gi, '<your-server>'],
[/WHP\d+(-[A-Z0-9]+)?\b/g, '<YOUR-SERVER>'],
// IP addresses
[/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '<your-server-IP>'],
// home dir + demo user
[/\/docker\/users\/[a-z0-9-]+/g, '/docker/users/<your-username>'],
[/demo-user/g, 'your-username'],
];
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
const nodes: Text[] = [];
let n: Node | null = walker.nextNode();
while (n) { nodes.push(n as Text); n = walker.nextNode(); }
for (const node of nodes) {
let v = node.nodeValue ?? '';
for (const [re, replacement] of swaps) v = v.replace(re, replacement);
if (v !== node.nodeValue) node.nodeValue = v;
}
});
}
async function captureShot(page: Page, shot: Shot): Promise<void> {
const viewport = shot.viewport ?? { width: 1440, height: 900 };
await page.setViewportSize(viewport);
@@ -36,6 +80,7 @@ async function captureShot(page: Page, shot: Shot): Promise<void> {
await page.goto(`${WHP_BASE}${shot.path}`);
await page.waitForLoadState('networkidle');
if (shot.waitFor) await page.waitForSelector(shot.waitFor);
await redactSensitive(page);
const maskSelectors = [...DEFAULT_MASK, ...(shot.mask ?? [])];
const maskLocators: Locator[] = maskSelectors.map((sel) => page.locator(sel));