f1159867df
The Email page is now organized into tabs (Email Accounts / Forwarders / Email Domains (DNS)) with a top button strip (Webmail / Admin Panel / Setup Instructions). Reworked the how-to to match: - orient readers to the tabs + top buttons; create on the Email Accounts tab - autodiscovery records now live in Email Domains (DNS) → Autodiscovery Records (DNS) (was "Mail Client Setup") - DKIM is in the DKIM Management section on the Email Domains (DNS) tab - Webmail / Setup Instructions are the top-strip buttons Recaptured whp-email.png (Email Accounts tab) and whp-email-autodiscovery.png (DNS tab) via the rewritten capture-email.ts (clicks the DNS tab; fleet hostnames/IPs redacted, brand demo domain kept). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
5.3 KiB
TypeScript
124 lines
5.3 KiB
TypeScript
/**
|
|
* Email capture — the tabbed Email Management page, as the demo customer.
|
|
*
|
|
* Captures:
|
|
* - whp-email.png the Email page on its default "Email Accounts"
|
|
* tab: the top button strip (Webmail / Admin
|
|
* Panel / Setup Instructions) + the tab bar
|
|
* (Email Accounts · Forwarders · Email Domains
|
|
* (DNS)) + the Email Accounts card.
|
|
* - whp-email-autodiscovery.png the "Autodiscovery Records (DNS)" card on the
|
|
* "Email Domains (DNS)" tab: the per-domain
|
|
* autodiscovery DNS records table + copyable zone.
|
|
*
|
|
* Viewport-only (1440x900, deviceScaleFactor 2), redacted for our multi-server
|
|
* fleet: server/mail hostnames + IPs become placeholders, while the brand demo
|
|
* domain (whp-demo.anhh.co) is kept visible on purpose.
|
|
*
|
|
* Read-only: switches tabs / selects a domain for the shot, never saves.
|
|
*/
|
|
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 HIDE_CSS = `.navbar-text, .brand-full { visibility: hidden !important; }`;
|
|
|
|
async function login(page: Page) {
|
|
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; mail/server hosts and IPs are
|
|
* swapped for placeholders. Inline only — no named helpers inside evaluate
|
|
* (esbuild's __name instrumentation isn't defined in the browser context).
|
|
*/
|
|
async function redact(page: Page) {
|
|
await page.addStyleTag({ content: HIDE_CSS });
|
|
await page.evaluate(() => {
|
|
const swaps: [RegExp, string][] = [
|
|
[/mail\d+\.cloud-hosting\.io/gi, '<mail-server>.cloud-hosting.io'],
|
|
[/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '<your-server>.cloud-hosting.io'],
|
|
[/WHP\d+(-[A-Z0-9]+)?\b/g, '<YOUR-SERVER>'],
|
|
[/whp\d+(-[a-z0-9]+)?\b/gi, '<your-server>'],
|
|
[/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '<server-IP>'],
|
|
];
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
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);
|
|
await page.goto(`${BASE}/index.php?page=email-management`, { waitUntil: 'networkidle' });
|
|
await page.waitForSelector('#email-mgmt-nav', { state: 'visible' });
|
|
|
|
// --- Shot 1: the default Email Accounts tab (orientation) ---
|
|
await page.waitForTimeout(300);
|
|
await redact(page);
|
|
await page.evaluate(() => window.scrollTo(0, 0));
|
|
const mainPath = resolve(OUT_DIR, 'whp-email.png');
|
|
await page.screenshot({ path: mainPath }); // viewport-only, no chrome
|
|
console.log(`captured whp-email -> ${mainPath}`);
|
|
|
|
// --- Shot 2: the Autodiscovery Records (DNS) card on the DNS tab ---
|
|
await page.locator('#email-mgmt-nav button[data-bs-target="#email-mgmt-dns"]').click();
|
|
await page.waitForSelector('#custMailDnsDomain', { state: 'visible' });
|
|
// Ensure a domain is selected, then (re)render the zone block.
|
|
await page.evaluate(() => {
|
|
const sel = document.getElementById('custMailDnsDomain') as HTMLSelectElement | null;
|
|
if (sel && sel.selectedIndex < 0 && sel.options.length) sel.selectedIndex = 0;
|
|
const fn = (window as unknown as { renderCustMailDns?: () => void }).renderCustMailDns;
|
|
if (typeof fn === 'function') fn();
|
|
});
|
|
await page.waitForTimeout(500);
|
|
await redact(page); // re-run: tab content rendered after the first pass
|
|
|
|
// The autodiscovery section is the .card wrapping the domain <select>.
|
|
const card = page.locator('#custMailDnsDomain').locator('xpath=ancestor::div[contains(@class,"card")][1]');
|
|
await card.scrollIntoViewIfNeeded();
|
|
await page.waitForTimeout(300);
|
|
const autoPath = resolve(OUT_DIR, 'whp-email-autodiscovery.png');
|
|
await card.screenshot({ path: autoPath });
|
|
console.log(`captured whp-email-autodiscovery -> ${autoPath}`);
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
|
|
main().catch((err) => { console.error(err); process.exit(1); });
|