Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
113c795c83 |
Binary file not shown.
|
Before Width: | Height: | Size: 307 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 288 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 332 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 358 KiB |
@@ -56,8 +56,6 @@ When you connect via SFTP, your account home (`/docker/users/<your-user>/`) show
|
||||
|
||||
Upload your files to the right place and the site picks them up immediately.
|
||||
|
||||
Logging in with your account password works out of the box. If you'd rather use an SSH key instead — or need to let a teammate connect without sharing your password — see [Manage SFTP SSH keys](/whp/how-to/manage-sftp-ssh-keys/).
|
||||
|
||||
## Connect to databases and caches
|
||||
|
||||
<Hostnames />
|
||||
@@ -84,7 +82,6 @@ Open your domain in a browser. You should see your site, or the default "no cont
|
||||
|
||||
- [Add a domain](/whp/how-to/add-a-domain/)
|
||||
- [Service hostnames](/whp/reference/service-hostnames/)
|
||||
- [Manage SFTP SSH keys](/whp/how-to/manage-sftp-ssh-keys/)
|
||||
|
||||
## Still stuck?
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
title: Manage SFTP SSH keys
|
||||
description: Add SSH public keys to your account so you — and anyone else on your team — can log in over SFTP without sharing a password.
|
||||
sidebar:
|
||||
order: 8
|
||||
---
|
||||
|
||||
import { Steps, Aside } from '@astrojs/starlight/components';
|
||||
import SignIn from '~/content/partials/signing-in.mdx';
|
||||
import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
SFTP normally logs in with your account password. SSH keys give you (or a teammate) a second way in — no password typing, and no need to share your password with anyone. Add one key per person, and remove any key on its own without touching anyone else's.
|
||||
|
||||
## Before you start
|
||||
|
||||
- An SSH key pair. If you don't have one yet, the steps below cover generating one — it takes about a minute.
|
||||
- About 5 minutes.
|
||||
|
||||
<Aside type="tip">
|
||||
The main reason to use this: if more than one person needs SFTP access to the same hosting account, everyone can add their own key instead of all sharing one password. Removing someone's access later is just removing their key.
|
||||
</Aside>
|
||||
|
||||
## Sign in to WHP
|
||||
|
||||
<SignIn />
|
||||
|
||||
## Steps
|
||||
|
||||
<Steps>
|
||||
|
||||
1. In the sidebar, click **Security → SFTP SSH Keys**.
|
||||

|
||||
|
||||
2. If you don't already have an SSH key, generate one on your own machine:
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "your-email-or-a-note-to-yourself"
|
||||
```
|
||||
|
||||
Accept the default file location (`~/.ssh/id_ed25519`) and set a passphrase if you'd like one. If you already have a key, you'll find it at `~/.ssh/id_ed25519.pub` (or `~/.ssh/id_rsa.pub` for an older RSA key).
|
||||
|
||||
<Aside type="caution">
|
||||
Only ever paste the **`.pub`** file's contents — that's the *public* key, safe to share. Never paste the file without `.pub` (or one that starts with `-----BEGIN OPENSSH PRIVATE KEY-----`) anywhere. The panel will refuse anything that looks like a private key, but it's worth knowing the difference.
|
||||
</Aside>
|
||||
|
||||
3. Copy the full contents of the `.pub` file — one line, starting with the key type (`ssh-ed25519`, `ssh-rsa`, etc.).
|
||||
|
||||
4. Paste it into the **Public key** box in the **Add Key** card. The **Note** field is optional — whatever you type there is recorded in the account's audit log, but it isn't what shows in the key list below (see the tip after this list for how to label a key so you can actually tell it apart later).
|
||||

|
||||
|
||||
5. Click **Add Key**. It appears in the **Authorized Keys** table immediately and works right away — no separate activation step.
|
||||

|
||||
|
||||
</Steps>
|
||||
|
||||
<Aside type="tip">
|
||||
The **Comment** column in the table comes from the key itself, not from the Note field — it's whatever followed the key type when the key was generated (the `-C "..."` value above, e.g. `your-email-or-a-note-to-yourself`). If several people are adding keys to the same account, agree on a naming convention for `-C` (e.g. `-C "dana-laptop"`) so everyone can tell the keys apart at a glance. A key generated without `-C` shows as **(no comment)**.
|
||||
</Aside>
|
||||
|
||||
## Connect over SFTP with your key
|
||||
|
||||
Point your SFTP client at the private key file (the one *without* `.pub`):
|
||||
|
||||
```bash
|
||||
sftp -i ~/.ssh/id_ed25519 <username>@<your-server>.cloud-hosting.io
|
||||
```
|
||||
|
||||
Replace `<username>` with your WHP username and `<your-server>` with your server's hostname (find it on the **Dashboard** page under Server Information). Graphical clients — FileZilla, Cyberduck, WinSCP — all support key-based login too: point the client's "private key" field at `~/.ssh/id_ed25519` instead of filling in a password.
|
||||
|
||||
Your account password still works for SFTP exactly as before. Adding a key doesn't disable it.
|
||||
|
||||
## Supported key types
|
||||
|
||||
- **`ssh-ed25519`** — recommended. Generate with `ssh-keygen -t ed25519`.
|
||||
- `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`
|
||||
- `ssh-rsa`
|
||||
- `sk-ssh-ed25519@openssh.com`, `sk-ecdsa-sha2-nistp256@openssh.com` — for a FIDO/hardware security key (e.g. a YubiKey)
|
||||
|
||||
If a key is malformed, an unsupported type, or technically well-formed but not something the SFTP server can actually use, the panel rejects it with an explanation instead of adding it — so a key that's been accepted is a key that works.
|
||||
|
||||
## Remove a key
|
||||
|
||||
Find the key in the **Authorized Keys** table and click **Remove**. Confirm in the dialog — the fingerprint and comment are shown so you can be sure you're removing the right one. This takes effect immediately: anyone using that key loses SFTP access right away, but password login is unaffected.
|
||||
|
||||
There's no restriction on removing your last key. If you remove every key on the account, SFTP still works with your account password — keys are additive, not a replacement for it.
|
||||
|
||||
Common reasons to remove a key: someone leaves the team, or a laptop with a key on it is lost or retired.
|
||||
|
||||
<Aside type="note">
|
||||
Keys can also be added or removed directly in `~/.ssh/authorized_keys` over SFTP itself, and valid changes made that way show up here too. But changes made outside this page aren't recorded in the audit log, and anything in that file the panel doesn't recognize (an unsupported key type, a malformed line, a restricted `command=`/`from=` entry) is cleaned up automatically the next time this page loads. Comments and blank lines are always left alone.
|
||||
</Aside>
|
||||
|
||||
## If you manage this account on someone else's behalf
|
||||
|
||||
Root and support staff reach the same page for any customer account — either from the same **Security → SFTP SSH Keys** sidebar entry (which then shows an account selector), or via a **SFTP Keys** button on that customer's row on the **User Management** page. Everything above works the same way once an account is selected; the only difference is picking whose keys you're managing first.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"This key is already present."** That exact key (by fingerprint) is already on the account — check the table before adding it again.
|
||||
|
||||
**Key added, but SFTP still asks for a password.** Double-check you're pointing your client at the matching **private** key file (no `.pub`), and that the username in your connection matches the account the key was added to.
|
||||
|
||||
**A key I added over raw SFTP disappeared.** If it didn't meet this page's rules (unsupported type, malformed, or carried a restricted option), it was removed the next time the page loaded — add it here instead so it's validated and recorded properly.
|
||||
|
||||
## Related
|
||||
|
||||
- [Create a site](/whp/how-to/create-a-site/) — including where your files go over SFTP.
|
||||
|
||||
## Still stuck?
|
||||
|
||||
<Support />
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Switching your site's backend
|
||||
description: Change the web engine (container type) running a site — standard PHP/FPM or the premium LiteSpeed/OpenLiteSpeed tier.
|
||||
description: Change the container type running a site — standard PHP/FPM, the premium LiteSpeed/OpenLiteSpeed tier, or Node.js.
|
||||
sidebar:
|
||||
order: 6
|
||||
---
|
||||
@@ -9,7 +9,7 @@ import { Steps, Aside } from '@astrojs/starlight/components';
|
||||
import SignIn from '~/content/partials/signing-in.mdx';
|
||||
import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
Every WHP site runs inside a container. The **container type** determines the web engine that serves your site's files and runs your PHP code. Most sites use a standard PHP or PHP-FPM container, which handles the vast majority of WordPress and PHP workloads well. If you've enabled the **Optimized Webserver** add-on, you also have access to LiteSpeed/OpenLiteSpeed (OLS) container types — a premium engine known for its built-in full-page cache (LSCache) and lower memory usage under traffic.
|
||||
Every WHP site runs inside a container. The **container type** determines the engine that serves your site: PHP, LiteSpeed, or Node.js. Most sites use a standard PHP or PHP-FPM container, which handles the vast majority of WordPress and PHP workloads well. If you've enabled the **Optimized Webserver** add-on, you also have access to LiteSpeed/OpenLiteSpeed (OLS) container types — a premium engine known for its built-in full-page cache (LSCache) and lower memory usage under traffic. If you're deploying your own application code instead of PHP, Node.js container types are also available — see [Node.js](#nodejs) below.
|
||||
|
||||
Switching backends is a one-step change in the Sites editor, but it does briefly restart your container, so plan for a few seconds of downtime.
|
||||
|
||||
@@ -36,6 +36,7 @@ Switching backends is a one-step change in the Sites editor, but it does briefly
|
||||
4. Select the backend you want:
|
||||
- **PHP** or **PHP-FPM** options — standard shared webserver tier, suitable for most WordPress and PHP sites.
|
||||
- **LiteSpeed PHP** options (e.g. *LiteSpeed PHP 8.x*) — premium OLS tier. These only appear if the Optimized Webserver add-on is active on your account.
|
||||
- **Node18**, **Node20**, or **Node22** — for sites running your own Node.js application code instead of PHP. See [Node.js](#nodejs) below before switching to one of these.
|
||||
|
||||
5. Click **Save**. WHP recreates the container with the new engine. Expect a brief moment of downtime (typically a few seconds) while the container restarts.
|
||||
|
||||
@@ -55,6 +56,24 @@ To enable it:
|
||||
2. Enable it from your [client portal](https://secure.anhonesthost.com/clientarea.php).
|
||||
3. Once active, the LiteSpeed PHP options will appear in the Container Type dropdown when editing any site.
|
||||
|
||||
## Node.js
|
||||
|
||||
Node.js container types (**Node18**, **Node20**, **Node22**) run your own application code instead of PHP — the container starts your app the same way you would locally with `npm start`. They're for React/Next.js servers, Express APIs, and other Node apps, not for WordPress or PHP sites.
|
||||
|
||||
<Aside type="caution">
|
||||
Switching an **existing PHP or WordPress site** to a Node container type won't make your PHP files run — Node containers don't execute PHP at all. Node backends are for sites where you're deploying your own Node.js application code. If you're starting a brand-new Node app rather than switching an existing site, [Create a site](/whp/how-to/create-a-site/) and choose **Node.js app** under "What are you building?" is the more direct path.
|
||||
</Aside>
|
||||
|
||||
A few things behave differently for Node compared to switching between PHP tiers:
|
||||
|
||||
- **No PHP-FPM / LiteSpeed choice.** Node containers bundle their own web server, so the shared-vs-standalone placement option that applies to PHP and LiteSpeed containers doesn't apply here — you won't see that toggle once a Node container type is selected.
|
||||
- **Higher minimum memory.** Node containers need more headroom than PHP containers. Selecting a Node container type in the editor bumps **Memory per Container** up to at least 512 MB (versus 256 MB for standard PHP). If your account doesn't have that much unused memory in its resource allowance, WHP will tell you and block the change until you free up resources or [upgrade your plan](/whp/add-ons/resource-upgrades/).
|
||||
- **Optional WebSocket / real-time support.** The Edit Site form has a **WebSocket / real-time support** checkbox alongside the Container Type field. Turn it on if your Node app holds connections open (Socket.IO, live chat, streaming) — it keeps connections alive for up to 6 hours instead of the standard 5-minute timeout. This is unrelated to the container type itself and works whether or not you just switched to Node.
|
||||
|
||||

|
||||
|
||||
Once you've switched and saved, upload your app to the `app/` folder for that domain via SFTP — see [Create a site](/whp/how-to/create-a-site/#where-your-files-go) for the file layout and how WHP starts your app.
|
||||
|
||||
## Before cancelling the Optimized Webserver add-on
|
||||
|
||||
<Aside type="caution">
|
||||
@@ -89,6 +108,10 @@ Once all sites are on standard backends, you can cancel or disable the add-on fr
|
||||
|
||||
**LiteSpeed cache not serving cached pages after the switch.** This is expected — the cache starts empty after every container recreation. It warms up automatically as visitors load pages.
|
||||
|
||||
**"Your account does not have enough resources left" when selecting a Node type.** Node containers need at least 512 MB of memory. Free up resources by removing or resizing another site, or [upgrade your plan](/whp/add-ons/resource-upgrades/).
|
||||
|
||||
**Site shows a 502 after switching to Node.** Give the container 15–30 seconds to start your app. If it's still down, confirm your app listens on the port WHP passes it via environment variable — see [Create a site](/whp/how-to/create-a-site/#troubleshooting).
|
||||
|
||||
## Related
|
||||
|
||||
- [Create a site](/whp/how-to/create-a-site/)
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
/**
|
||||
* SFTP SSH Keys capture — the new customer-facing key management page
|
||||
* (WHP release 2026.09.11), as the demo customer.
|
||||
*
|
||||
* Captures:
|
||||
* - whp-sftp-keys-empty.png Authorized Keys list with no keys yet
|
||||
* (the real empty state).
|
||||
* - whp-sftp-keys-add-form.png the Add Key form filled in with a
|
||||
* throwaway demo key, before submitting.
|
||||
* - whp-sftp-keys-list.png Authorized Keys list after the key was
|
||||
* added — fingerprint/type/comment/added
|
||||
* columns populated.
|
||||
*
|
||||
* This is a documentation-owned demo account (demo-user), so unlike the other
|
||||
* capture-*.ts scripts, this one DOES submit the Add Key / Remove actions —
|
||||
* that's the only way to show the populated list state, and the account is
|
||||
* reset back to empty at the end of the run (no key is left behind).
|
||||
*
|
||||
* Viewport-only (1440x900, deviceScaleFactor 2), redacted for our multi-server
|
||||
* fleet: server hostnames/IPs become placeholders, while the brand demo
|
||||
* account (demo-user) is kept visible on purpose.
|
||||
*/
|
||||
import { chromium, type Page } from 'playwright';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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>'],
|
||||
[/\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;
|
||||
}
|
||||
document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>('input, textarea').forEach((el) => {
|
||||
if ((el as HTMLInputElement).type === 'password' || !el.value) return;
|
||||
let v = el.value;
|
||||
const swaps2: [RegExp, string][] = [
|
||||
[/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '<your-server>.cloud-hosting.io'],
|
||||
[/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '<server-IP>'],
|
||||
];
|
||||
for (const [re, rep] of swaps2) 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 mkdir(OUT_DIR, { recursive: true });
|
||||
|
||||
// Throwaway ed25519 keypair, generated fresh for this capture only. Never
|
||||
// used to actually connect; discarded with the OS temp dir.
|
||||
const os = await import('node:os');
|
||||
const fs = await import('node:fs/promises');
|
||||
const tmpDir = await fs.mkdtemp(resolve(os.tmpdir(), 'kb-sftp-demo-key-'));
|
||||
const keyPath = resolve(tmpDir, 'id_ed25519');
|
||||
execFileSync('ssh-keygen', ['-t', 'ed25519', '-N', '', '-C', "dana's-laptop", '-f', keyPath]);
|
||||
const pubKey = (await fs.readFile(`${keyPath}.pub`, 'utf8')).trim();
|
||||
|
||||
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);
|
||||
|
||||
// 1. SFTP SSH Keys page — empty state
|
||||
await page.goto(`${BASE}/index.php?page=sftp-keys`, { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(600);
|
||||
await shot(page, 'whp-sftp-keys-empty');
|
||||
|
||||
// 2. Add Key form, filled in but not yet submitted
|
||||
await page.fill('#sftp-key-input', pubKey);
|
||||
await page.fill('#sftp-key-note', "Dana's laptop");
|
||||
await shot(page, 'whp-sftp-keys-add-form');
|
||||
|
||||
// 3. Submit, then capture the populated list
|
||||
await page.click('#sftp-keys-add-btn');
|
||||
await page.waitForTimeout(1200);
|
||||
await shot(page, 'whp-sftp-keys-list');
|
||||
|
||||
// Clean up: remove the demo key so the account is left as it was found.
|
||||
page.once('dialog', (d) => d.accept());
|
||||
await page.locator('button:has-text("Remove")').first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
console.log('cleanup: demo key removed');
|
||||
} finally {
|
||||
await browser.close();
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => { console.error(err); process.exit(1); });
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user