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:
98
tools/screenshots/_discovered-nav.json
Normal file
98
tools/screenshots/_discovered-nav.json
Normal file
@@ -0,0 +1,98 @@
|
||||
[
|
||||
{
|
||||
"text": "Logout",
|
||||
"href": "index.php?whp-action=logout"
|
||||
},
|
||||
{
|
||||
"text": "Dashboard",
|
||||
"href": "index.php?page=dashboard"
|
||||
},
|
||||
{
|
||||
"text": "Sites",
|
||||
"href": "index.php?page=sites"
|
||||
},
|
||||
{
|
||||
"text": "Traffic",
|
||||
"href": "index.php?page=site-traffic"
|
||||
},
|
||||
{
|
||||
"text": "Site Builder BETA",
|
||||
"href": "index.php?page=site-builder"
|
||||
},
|
||||
{
|
||||
"text": "Domains",
|
||||
"href": "index.php?page=domains"
|
||||
},
|
||||
{
|
||||
"text": "WordPress",
|
||||
"href": "index.php?page=wordpress"
|
||||
},
|
||||
{
|
||||
"text": "File Manager",
|
||||
"href": "./filemanager/"
|
||||
},
|
||||
{
|
||||
"text": "Terminal",
|
||||
"href": "index.php?page=terminal"
|
||||
},
|
||||
{
|
||||
"text": "cPanel Import",
|
||||
"href": "index.php?page=cpanel-import"
|
||||
},
|
||||
{
|
||||
"text": "MySQL Management",
|
||||
"href": "index.php?page=database-management"
|
||||
},
|
||||
{
|
||||
"text": "phpMyAdmin",
|
||||
"href": "./phpmyadmin-sso.php"
|
||||
},
|
||||
{
|
||||
"text": "PostgreSQL",
|
||||
"href": "index.php?page=postgresql-management"
|
||||
},
|
||||
{
|
||||
"text": "Adminer",
|
||||
"href": "./adminer-sso.php"
|
||||
},
|
||||
{
|
||||
"text": "Valkey Cache",
|
||||
"href": "index.php?page=account-valkey"
|
||||
},
|
||||
{
|
||||
"text": "Email",
|
||||
"href": "index.php?page=email-management"
|
||||
},
|
||||
{
|
||||
"text": "Backups",
|
||||
"href": "index.php?page=backups"
|
||||
},
|
||||
{
|
||||
"text": "Site Monitoring",
|
||||
"href": "index.php?page=ai-monitor-customer"
|
||||
},
|
||||
{
|
||||
"text": "Delegated Users",
|
||||
"href": "index.php?page=delegated-users"
|
||||
},
|
||||
{
|
||||
"text": "Active Sessions",
|
||||
"href": "index.php?page=active-sessions"
|
||||
},
|
||||
{
|
||||
"text": "Cloud Apache Container",
|
||||
"href": "https://repo.anhonesthost.net/cloud-hosting-platform/cloud-apache-container"
|
||||
},
|
||||
{
|
||||
"text": "Cloud Node Container",
|
||||
"href": "https://repo.anhonesthost.net/cloud-hosting-platform/cloud-node-container"
|
||||
},
|
||||
{
|
||||
"text": "Manage Domains",
|
||||
"href": "index.php?page=domains"
|
||||
},
|
||||
{
|
||||
"text": "Manage Databases",
|
||||
"href": "index.php?page=database-management"
|
||||
}
|
||||
]
|
||||
109
tools/screenshots/discover.ts
Normal file
109
tools/screenshots/discover.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Discovery script — logs in, captures the dashboard, and probes the sidebar
|
||||
* nav to learn the actual URLs of the sections referenced in our docs.
|
||||
*
|
||||
* Not committed for ongoing use; the canonical capture is run.ts.
|
||||
*/
|
||||
import { chromium, type Page } from 'playwright';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const OUT_DIR = resolve(__dirname, '../../src/assets/screenshots/whp');
|
||||
|
||||
function need(name: string): string {
|
||||
const v = process.env[name];
|
||||
if (!v) throw new Error(`missing env: ${name}`);
|
||||
return v;
|
||||
}
|
||||
|
||||
const BASE = need('WHP_BASE');
|
||||
const USER = need('WHP_USER');
|
||||
const PASS = need('WHP_PASS');
|
||||
|
||||
async function login(page: Page): Promise<void> {
|
||||
console.log(`navigating ${BASE}/login.php`);
|
||||
await page.goto(`${BASE}/login.php`, { waitUntil: 'domcontentloaded' });
|
||||
console.log(`landed at ${page.url()}`);
|
||||
|
||||
await page.fill('input[name="user"]', USER);
|
||||
await page.fill('input[name="password"]', PASS);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState('networkidle');
|
||||
console.log(`logged in, now at ${page.url()}`);
|
||||
}
|
||||
|
||||
async function snapshotPage(page: Page, id: string): Promise<void> {
|
||||
await mkdir(OUT_DIR, { recursive: true });
|
||||
const path = resolve(OUT_DIR, `${id}.png`);
|
||||
await page.screenshot({ path, fullPage: false });
|
||||
console.log(`captured ${id} -> ${path}`);
|
||||
}
|
||||
|
||||
async function listNavLinks(page: Page): Promise<{ text: string; href: string | null }[]> {
|
||||
// Use locator API to collect all anchors with hrefs (no string-eval).
|
||||
const anchors = page.locator('a[href]');
|
||||
const count = await anchors.count();
|
||||
const out: { text: string; href: string | null }[] = [];
|
||||
for (let i = 0; i < count && out.length < 120; i++) {
|
||||
const a = anchors.nth(i);
|
||||
const href = await a.getAttribute('href');
|
||||
if (!href || href.startsWith('#')) continue;
|
||||
const text = ((await a.textContent()) ?? '').trim().slice(0, 60);
|
||||
if (!text) continue;
|
||||
out.push({ text, href });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function probe(page: Page, label: string, hrefHint: string): Promise<string | null> {
|
||||
const link = page.locator(`a[href*="${hrefHint}"]`).first();
|
||||
if ((await link.count()) === 0) {
|
||||
console.log(` NO MATCH for "${label}" (hint=${hrefHint})`);
|
||||
return null;
|
||||
}
|
||||
const href = await link.getAttribute('href');
|
||||
console.log(` ${label}: ${href}`);
|
||||
return href;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const ctx = await browser.newContext({
|
||||
ignoreHTTPSErrors: true,
|
||||
viewport: { width: 1440, height: 900 },
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
|
||||
try {
|
||||
await login(page);
|
||||
await snapshotPage(page, '_discovery-dashboard');
|
||||
|
||||
const navLinks = await listNavLinks(page);
|
||||
await writeFile(
|
||||
resolve(__dirname, '_discovered-nav.json'),
|
||||
JSON.stringify(navLinks, null, 2),
|
||||
);
|
||||
console.log(`wrote ${navLinks.length} nav links to _discovered-nav.json`);
|
||||
|
||||
for (const [label, hint] of [
|
||||
['Domains', 'domain'],
|
||||
['Sites', 'site'],
|
||||
['Email', 'email'],
|
||||
['Backups', 'backup'],
|
||||
['Monitor', 'monitor'],
|
||||
['Resources', 'resource'],
|
||||
['Dashboard', 'dashboard'],
|
||||
] as const) {
|
||||
await probe(page, label, hint);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
53
tools/screenshots/peek.ts
Normal file
53
tools/screenshots/peek.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* One-shot peek script: log in, navigate to a specific URL, screenshot it.
|
||||
* Usage: PEEK_URL=... npx tsx tools/screenshots/peek.ts
|
||||
*/
|
||||
import { chromium, type Page } from 'playwright';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const OUT_DIR = resolve(__dirname, '../../src/assets/screenshots/whp');
|
||||
|
||||
function need(name: string): string {
|
||||
const v = process.env[name];
|
||||
if (!v) throw new Error(`missing env: ${name}`);
|
||||
return v;
|
||||
}
|
||||
|
||||
const BASE = need('WHP_BASE');
|
||||
const USER = need('WHP_USER');
|
||||
const PASS = need('WHP_PASS');
|
||||
const PEEK = need('PEEK_URL');
|
||||
const ID = process.env.PEEK_ID ?? '_peek';
|
||||
|
||||
async function login(page: Page): Promise<void> {
|
||||
await page.goto(`${BASE}/login.php`, { waitUntil: 'domcontentloaded' });
|
||||
await page.fill('input[name="user"]', USER);
|
||||
await page.fill('input[name="password"]', PASS);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
await login(page);
|
||||
await page.goto(`${BASE}${PEEK}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await mkdir(OUT_DIR, { recursive: true });
|
||||
const out = resolve(OUT_DIR, `${ID}.png`);
|
||||
await page.screenshot({ path: out, fullPage: true });
|
||||
console.log(`captured ${PEEK} -> ${out}`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -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));
|
||||
|
||||
@@ -15,19 +15,18 @@ export type Shot = {
|
||||
|
||||
/** Always-applied redactions. Per-shot mask is added on top of this list. */
|
||||
export const DEFAULT_MASK: string[] = [
|
||||
'[data-test="account-id"]',
|
||||
'[data-test="server-hostname"]',
|
||||
'[data-test="user-ip"]',
|
||||
'.billing-column',
|
||||
// Conservative defaults — the actual selectors may differ; verify by inspecting
|
||||
// a captured shot and adding more selectors here if anything leaks.
|
||||
];
|
||||
|
||||
export const shots: Shot[] = [
|
||||
{ id: 'whp-domains-add', path: '/domains/add' },
|
||||
{ id: 'whp-sites-add', path: '/sites/add' },
|
||||
{ id: 'whp-email-add', path: '/email/add' },
|
||||
{ id: 'whp-backups-settings', path: '/backups/settings' },
|
||||
{ id: 'whp-backups-history', path: '/backups/history' },
|
||||
{ id: 'whp-monitor', path: '/security/monitor' },
|
||||
{ id: 'whp-email-archive', path: '/email/archive' },
|
||||
{ id: 'whp-resources', path: '/overview/resources' },
|
||||
{ id: 'whp-dashboard', path: '/index.php?page=dashboard' },
|
||||
{ id: 'whp-domains', path: '/index.php?page=domains' },
|
||||
{ id: 'whp-sites', path: '/index.php?page=sites' },
|
||||
{ id: 'whp-email', path: '/index.php?page=email-management' },
|
||||
{ id: 'whp-backups', path: '/index.php?page=backups' },
|
||||
{ id: 'whp-site-monitoring', path: '/index.php?page=ai-monitor-customer' },
|
||||
{ id: 'whp-mysql', path: '/index.php?page=database-management' },
|
||||
{ id: 'whp-postgres', path: '/index.php?page=postgresql-management' },
|
||||
{ id: 'whp-valkey', path: '/index.php?page=account-valkey' },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user