/** * Sites "backend" capture — the Edit Site modal's Container Type field, * captured with a Node.js container type selected (for the "switching your * site's backend" how-to's Node.js section). * * Captures, as the demo customer: * - whp-sites-edit-node.png Edit Site modal, Container Type = Node22, * showing the auto-adjusted Memory per * Container / Total Resources readout. * * Viewport-only (1440x900), redacted for our multi-server fleet: server / * mail / nameserver hostnames, IPs, and the navbar brand strip become * neutral or hidden, while the brand demo domain (whp-demo.anhh.co) stays * visible on purpose. * * Read-only: opens the Edit modal and changes the Container Type dropdown * for the shot, but never clicks Save. */ import { chromium, type Page } from 'playwright'; import { mkdir, readFile } 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'); async function loadEnv(): Promise { const envPath = resolve(__dirname, '.env'); const content = await readFile(envPath, 'utf-8'); for (const line of content.split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eq = trimmed.indexOf('='); if (eq === -1) continue; const key = trimmed.slice(0, eq).trim(); const val = trimmed.slice(eq + 1).trim(); if (!process.env[key]) process.env[key] = val; } } function need(name: string): string { const v = process.env[name]; if (!v) throw new Error(`missing env: ${name}`); return v; } const HIDE_CSS = `.navbar-text, .brand-full { visibility: hidden !important; }`; async function login(page: Page, base: string, user: string, pass: string) { 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'); } /** * Neutralise fleet-identifying text before the screenshot. The brand demo * domain (anhh.co) is intentionally preserved; everything that names a * specific server, mail host, nameserver, or IP is swapped for a placeholder. */ async function redact(page: Page) { await page.addStyleTag({ content: HIDE_CSS }); await page.evaluate(() => { const swaps: [RegExp, string][] = [ [/ns[12]\.whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, 'ns..cloud-hosting.io'], [/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '.cloud-hosting.io'], [/mail\d+\.cloud-hosting\.io/gi, '.cloud-hosting.io'], [/WHP\d+(-[A-Z0-9]+)?\b/g, ''], [/whp\d+(-[a-z0-9]+)?\b/gi, ''], // Public IPv4 (skip RFC1918 — those read fine as generic examples) [/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, ''], [/demo-user/g, 'your-username'], ]; const walker = document.createTreeWalker(document.body, 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, rep] of swaps) v = v.replace(re, rep); if (v !== node.nodeValue) node.nodeValue = v; } document.querySelectorAll('input').forEach((el) => { if (el.type === 'password' || !el.value) return; let v = el.value; for (const [re, rep] of swaps) v = v.replace(re, rep); if (v !== el.value) el.value = v; }); }); } async function shot(page: Page, id: string) { await page.waitForTimeout(400); await redact(page); const path = resolve(OUT_DIR, `${id}.png`); await page.screenshot({ path, fullPage: false }); console.log(`captured ${id} -> ${path}`); } async function main() { await loadEnv(); const BASE = need('WHP_BASE'); const USER = need('WHP_USER'); const PASS = need('WHP_PASS'); const DOMAIN = process.env.WHP_DEMO_DOMAIN ?? 'whp-demo.anhh.co'; await mkdir(OUT_DIR, { recursive: true }); const browser = await chromium.launch({ headless: true }); const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2, }); const page = await ctx.newPage(); try { await login(page, BASE, USER, PASS); await page.goto(`${BASE}/index.php?page=sites`, { waitUntil: 'networkidle' }); // Select the demo site from the "Select a Site" widget (Choices.js). const siteChoicesContainer = page.locator('div.choices:has(#siteDropdown)').first(); await siteChoicesContainer.click(); await page.waitForTimeout(300); const item = page .locator('.choices__list--dropdown .choices__item--choice', { hasText: DOMAIN.split('.')[0] }) .first(); await item.click(); // Wait for the async site-details fetch, then open Edit (Manage). await page.locator('#siteActionsContainer').waitFor({ state: 'visible', timeout: 10000 }); await page.waitForTimeout(300); await page.locator('#manageSiteBtn').click(); await page.waitForTimeout(800); // Switch the Container Type to Node22 (the newest of the three Node // options) to show the memory floor auto-adjust to 512 MB. const editSelect = page.locator('#edit_container_type_id'); await editSelect.waitFor({ state: 'visible', timeout: 10000 }); await editSelect.scrollIntoViewIfNeeded(); const nodeOption = editSelect.locator('option', { hasText: 'Node22' }); const nodeValue = await nodeOption.getAttribute('value'); if (!nodeValue) throw new Error('Node22 option not found in edit_container_type_id'); await editSelect.selectOption(nodeValue); await page.waitForTimeout(600); await shot(page, 'whp-sites-edit-node'); } finally { await browser.close(); } } main().catch((err) => { console.error(err); process.exit(1); });