The "Switching your site's backend" page only covered PHP/PHP-FPM and the LiteSpeed/OLS add-on tier. Node18/Node20/Node22 are also selectable in the same Container Type dropdown, but weren't mentioned. Adds a Node.js section covering the npm-start model, the auto-raised 512 MB memory floor, the WebSocket/real-time checkbox, and that Node containers skip the shared PHP-FPM/LiteSpeed placement choice — plus a capture script and screenshot of the Edit Site modal with Node22 selected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
157 lines
6.0 KiB
TypeScript
157 lines
6.0 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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<n>.<your-server>.cloud-hosting.io'],
|
|
[/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '<your-server>.cloud-hosting.io'],
|
|
[/mail\d+\.cloud-hosting\.io/gi, '<mail-server>.cloud-hosting.io'],
|
|
[/WHP\d+(-[A-Z0-9]+)?\b/g, '<YOUR-SERVER>'],
|
|
[/whp\d+(-[a-z0-9]+)?\b/gi, '<your-server>'],
|
|
// 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, '<server-IP>'],
|
|
[/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<HTMLInputElement>('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);
|
|
});
|