Author SHA1 Message Date
jknapp 8c515a8074 Merge branch 'main' into docs/bichon-opensource-link 2026-06-29 01:30:55 +00:00
shadowdaoandClaude Opus 4.8 5fdf55de7f docs: link Bichon to its open-source project
The archival-email page linked to https://anhonesthost.com/bichon/, which
does not exist. Point readers at the upstream open-source project instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:29:33 -07:00
jknapp f7d739fe2d Merge pull request 'docs(how-to): add 'Clear your site's cache' guide' (#7) from docs/clear-your-cache into main
Build and deploy / deploy (push) Successful in 25s
Reviewed-on: #7
2026-06-27 04:04:58 +00:00
shadowdaoandClaude Opus 4.8 858d505e7e docs(how-to): add 'Clear your site's cache' guide
Customer-facing troubleshooting for stale content: hard refresh (browser cache)
first, then purge LiteSpeed Cache via the WP plugin Toolbox for Optimized
Webserver sites, with a note that logged-in views are never cached. Cross-linked
from the Optimized Webserver add-on page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 21:02:17 -07:00
jknapp 240b6d392b Merge pull request 'docs(email): fix webmail-reachability note and SMTP port guidance' (#5) from docs/email-port-webmail-fixes into main
Build and deploy / deploy (push) Successful in 22s
Reviewed-on: #5
2026-06-23 22:15:01 +00:00
jknapp 9762822e97 Merge branch 'main' into docs/email-port-webmail-fixes 2026-06-23 22:14:52 +00:00
jknapp 54bee2cf55 Merge pull request 'docs(archival): retention is for the life of the account, not 14 days' (#6) from docs/archival-retention into main
Build and deploy / deploy (push) Successful in 24s
Reviewed-on: #6
2026-06-23 22:14:46 +00:00
shadowdaoandClaude Opus 4.8 ef4a4605b2 docs(archival): correct retention — kept for the life of the account
The add-on does not impose a 14-day window or offer "configurable retention."
Archived mail is retained for as long as the customer has an active account
with us. Replaced the inaccurate "14-day quick-restore window" and
"Configurable longer retention" highlights, and softened the compliance
use-case wording away from "fixed window."

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:13:55 -07:00
shadowdaoandClaude Opus 4.8 3b31acbbac docs(email): fix webmail-reachability note and outgoing SMTP port guidance
- Webmail is hosted at our address (the Webmail button opens it directly), so
  it does not depend on the customer's domain/DNS. Replaced the incorrect
  "DNS still propagating" troubleshooting note.
- Outgoing SMTP: lead with port 465 (SSL/TLS) as the standard and present 587
  (STARTTLS) as the alternate submission port; note that port 25 is for
  server-to-server and shouldn't be used from a mail client. Updated the
  IMAP-but-not-SMTP troubleshooting entry to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:09:09 -07:00
jknapp 3a526783cb Merge pull request 'docs(email): update Create an email account for the tabbed layout' (#4) from docs/email-tabs into main
Build and deploy / deploy (push) Successful in 25s
Reviewed-on: #4
2026-06-23 21:56:46 +00:00
shadowdaoandClaude Opus 4.8 1769d5dc0b docs(email): show the create-account form in the Email Accounts screenshot
Bumped the demo account's email-account allowance so the page renders the
"Create Email Account" button + usage bar instead of the limit-reached state.
Updated steps to match the button → modal flow (Create Email Account opens the
form; the modal's submit button is "Create Account").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:54:29 -07:00
shadowdaoandClaude Opus 4.8 f1159867df docs(email): update Create an email account for the new tabbed layout
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>
2026-06-23 14:49:33 -07:00
jknapp a6269d18fd Merge pull request 'docs(screenshots): document section capture scripts' (#3) from docs/dns-page-rework into main
Build and deploy / deploy (push) Successful in 22s
Reviewed-on: #3
2026-06-22 15:43:02 +00:00
shadowdaoandClaude Opus 4.8 8f42adc799 docs(screenshots): list capture-email.ts in the section-scripts table
Completes the section-capture docs after merging main — capture-email.ts
(Email page "Mail Client Setup") now appears alongside the other scripts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 08:41:06 -07:00
shadowdao 7f63f064c9 Merge branch 'main' into docs/dns-page-rework 2026-06-22 08:40:31 -07:00
shadowdaoandClaude Opus 4.8 23cc5a887b docs(screenshots): document section capture scripts + refresh workflow
The README only covered the shots.config.ts/run.ts path. Add a Section
capture scripts table (capture-admin/site-builder/dns) and a refresh
note distinguishing static pages (npm run screenshots) from interactive
states (npx tsx capture-<section>.ts), since reworked sections need the
latter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:00:14 -07:00
9 changed files with 273 additions and 42 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 161 KiB

@@ -15,10 +15,10 @@ import Support from '~/content/partials/support-link.mdx';
Archival email keeps a long-term, searchable copy of your mail **outside** the live mailbox. It's useful when: Archival email keeps a long-term, searchable copy of your mail **outside** the live mailbox. It's useful when:
- You need to retain mail beyond your live mailbox's storage cap. - You need to retain mail beyond your live mailbox's storage cap.
- Compliance or policy requires you keep email for a fixed window. - Compliance or policy requires you keep email long-term.
- You want a recovery option for mail you accidentally delete from the live mailbox. - You want a recovery option for mail you accidentally delete from the live mailbox.
It's powered by our [Bichon](https://anhonesthost.com/bichon/) archival service. It's powered by the open-source [Bichon](https://github.com/rustmailer/bichon) archival service.
## How it's different from backups ## How it's different from backups
@@ -33,8 +33,8 @@ You can use both — they cover different problems.
## What's included ## What's included
- **Per-mailbox archive.** Enable on the mailboxes that need it, not the whole account. Your plan has an **archival slots** quota; the Email page shows current usage (e.g. `Archival: 0 of 0 mailboxes archived (no archival slots in your plan)` if you haven't added the add-on yet). - **Per-mailbox archive.** Enable on the mailboxes that need it, not the whole account. Your plan has an **archival slots** quota; the Email page shows current usage (e.g. `Archival: 0 of 0 mailboxes archived (no archival slots in your plan)` if you haven't added the add-on yet).
- **14-day quick-restore window.** Accidentally deleted mail is recoverable without staff help. - **Retained for the life of your account.** We keep your archive for as long as you have an active account with us — there's no 14-day limit or fixed expiry window.
- **Configurable longer retention.** Set retention to match your policy. - **Self-service restore.** Mail you accidentally delete from the live mailbox stays in the archive, so you can find and recover it yourself without staff help.
- **Independent password reset on the archive.** Grant audit access without disturbing the live mailbox. - **Independent password reset on the archive.** Grant audit access without disturbing the live mailbox.
## How to enable ## How to enable
@@ -83,6 +83,7 @@ For the site-switching steps, see [Switching a site's backend](/whp/how-to/switc
## Related ## Related
- [Clear your site's cache](/whp/how-to/clear-your-cache/) — what to do when a change doesn't show up right away.
- [Add-ons overview](/whp/add-ons/overview/) - [Add-ons overview](/whp/add-ons/overview/)
- [Resource upgrades](/whp/add-ons/resource-upgrades/) — if you need more CPU or RAM rather than a faster cache layer. - [Resource upgrades](/whp/add-ons/resource-upgrades/) — if you need more CPU or RAM rather than a faster cache layer.
- [Site Monitoring](/whp/add-ons/monitoring/) — pair with Optimized Webserver to catch any cache-related issues early. - [Site Monitoring](/whp/add-ons/monitoring/) — pair with Optimized Webserver to catch any cache-related issues early.
@@ -0,0 +1,71 @@
---
title: Clear your site's cache
description: Seeing an old version of a page after making a change? Here's how to clear cached content so your updates show up.
sidebar:
order: 7
---
import { Steps, Aside } from '@astrojs/starlight/components';
import Support from '~/content/partials/support-link.mdx';
If you've changed something on your site but still see the old version — an updated page, a new image, a price, a published post — it's almost always **caching**: a saved copy of the page is being shown to make your site fast. Clearing the cache tells the system to build a fresh copy.
There are two places a saved copy can live: in **your browser**, and on the **server** (if your site uses our [Optimized Webserver](/whp/add-ons/optimized-webserver/) add-on with LiteSpeed Cache). Work through the steps below in order — the first one fixes most cases.
<Aside type="note">
When you're **logged in** to WordPress, we never serve you a cached page — you always see your site exactly as it is right now. So if a change looks missing while you're logged in, it's almost certainly your browser holding an old copy. Start with a hard refresh.
</Aside>
## Start with a hard refresh
A normal refresh often reloads the page from your browser's own saved copy. A *hard* refresh forces your browser to fetch everything fresh from the server.
<Steps>
1. Open the page that looks out of date.
2. Do a hard refresh:
- **Windows / Linux:** press `Ctrl` + `Shift` + `R`
- **Mac:** press `Cmd` + `Shift` + `R`
3. Still seeing the old version? Open the same page in a **private / incognito window** (which ignores your browser cache entirely). If it looks correct there, the issue was just your browser — clear your browser cache and you're done.
</Steps>
## Purge the server cache
If your site is on the **Optimized Webserver** add-on, pages are also cached at the server by LiteSpeed Cache. Most of the time this clears itself automatically — publishing a post, updating a page, or completing an order purges the right pages for you. Occasionally (after a large change, a theme edit, or a bulk update) you may want to clear it by hand.
For WordPress sites, you do this from the free **LiteSpeed Cache** plugin:
<Steps>
1. Sign in to your site's **WordPress admin** (`yourdomain.com/wp-admin`).
2. In the left menu, go to **LiteSpeed Cache → Toolbox**.
3. On the **Purge** tab, click **Purge All**. This drops every cached page; the next visitor to each page gets a freshly built copy.
</Steps>
<Aside type="tip">
There's also a shortcut in the black toolbar at the top of every WordPress admin page: the **LiteSpeed Cache** menu has a **Purge All** option you can use without leaving the page you're on.
</Aside>
<Aside type="caution">
The very first visit to each page after a purge runs at normal (uncached) speed while the fresh copy is built — then it's fast again. Don't judge your site's speed on that first load right after purging.
</Aside>
## If that didn't fix it
- **Your site isn't WordPress**, or you don't have the LiteSpeed Cache plugin — there's nothing for you to purge directly. Contact us and we'll clear the server-side cache for you.
- **You purged everything and still see the old version** — give it a moment and try a hard refresh again. If it persists, it may not be a cache issue at all (for example, a change that didn't actually save, or a content/plugin problem). Reach out and we'll take a look.
## Related
- [Optimized Webserver (OpenLiteSpeed + LSCache)](/whp/add-ons/optimized-webserver/) — what the server-level cache is and how to enable it.
- [Switching a site's backend](/whp/how-to/switching-site-backend/)
## Still stuck?
<Support />
@@ -23,14 +23,14 @@ import Support from '~/content/partials/support-link.mdx';
<Steps> <Steps>
1. In the sidebar, click **Email**. 1. In the sidebar, click **Email**. The page is organized into tabs — **Email Accounts**, **Forwarders**, and **Email Domains (DNS)** — and opens on **Email Accounts**. The buttons along the top (**Webmail**, **Admin Panel**, **Setup Instructions**) open the mail server's web tools in a new tab.
![WHP Email Management page](~/assets/screenshots/whp/whp-email.png) ![The WHP Email page on the Email Accounts tab, showing the tab bar and the top access buttons](~/assets/screenshots/whp/whp-email.png)
2. Scroll to **Email Accounts** and use the form to create a new account on one of your domains. You'll be asked for the domain, the local part, a password, and an optional mailbox size cap. 2. On the **Email Accounts** tab, click **Create Email Account** to open the new-account form. You'll be asked for the domain, the local part, a password, and an optional mailbox size cap.
3. Set a **strong password** — at least 12 characters with a mix of upper case, lower case, numbers, and symbols. Email accounts are common attack targets. 3. Set a **strong password** — at least 12 characters with a mix of upper case, lower case, numbers, and symbols. Email accounts are common attack targets.
4. Click **Create**. The new account appears in the **Email Accounts** list. 4. Click **Create Account**. The new account appears in the **Email Accounts** list.
</Steps> </Steps>
@@ -42,9 +42,9 @@ Most modern mail apps — Outlook, Apple Mail, Thunderbird, and the iOS and Andr
### If your DNS is hosted elsewhere ### If your DNS is hosted elsewhere
If your domain's DNS lives at another provider (Cloudflare, GoDaddy, Namecheap, and so on), your mail app can't auto-configure until you add a few records there yourself. The Email page builds the exact records for you: open the **Mail Client Setup** section, pick the domain, and copy them in. If your domain's DNS lives at another provider (Cloudflare, GoDaddy, Namecheap, and so on), your mail app can't auto-configure until you add a few records there yourself. The Email page builds the exact records for you: open the **Email Domains (DNS)** tab, find **Autodiscovery Records (DNS)**, pick the domain, and copy them in.
![The Mail Client Setup section on the WHP Email page, showing autodiscovery DNS records for a domain](~/assets/screenshots/whp/whp-email-autodiscovery.png) ![The Autodiscovery Records (DNS) section on the Email Domains (DNS) tab, showing autodiscovery DNS records for a domain](~/assets/screenshots/whp/whp-email-autodiscovery.png)
Add these records to the domain's zone at your DNS provider. The names are **relative to your domain** — most providers fill in the rest automatically, so `autoconfig` becomes `autoconfig.example.com`. Add these records to the domain's zone at your DNS provider. The names are **relative to your domain** — most providers fill in the rest automatically, so `autoconfig` becomes `autoconfig.example.com`.
@@ -56,7 +56,7 @@ Add these records to the domain's zone at your DNS provider. The names are **rel
| SRV | `_submission._tcp` | 0 | 1 | 587 | your mail server | | SRV | `_submission._tcp` | 0 | 1 | 587 | your mail server |
| SRV | `_pop3s._tcp` | 0 | 1 | 995 | your mail server | | SRV | `_pop3s._tcp` | 0 | 1 | 995 | your mail server |
Use the **mail server hostname shown in the Mail Client Setup section** as the value — it's the same host your **MX** record points at. The `_pop3s` record is only needed if you read mail over POP3 instead of IMAP. Click **Copy records** to grab them all at once in zone-file format. Use the **mail server hostname shown in the Autodiscovery Records (DNS) section** as the value — it's the same host your **MX** record points at. The `_pop3s` record is only needed if you read mail over POP3 instead of IMAP. Click **Copy records** to grab them all at once in zone-file format.
<Aside type="tip"> <Aside type="tip">
If your provider has a proxy toggle (such as Cloudflare's orange cloud), keep these records **DNS only** — proxying them stops mail clients from reading them. If your provider has a proxy toggle (such as Cloudflare's orange cloud), keep these records **DNS only** — proxying them stops mail clients from reading them.
@@ -68,7 +68,7 @@ Use the **mail server hostname shown in the Mail Client Setup section** as the v
## Set up your email client ## Set up your email client
Most apps configure themselves from the records above once you enter your address and password. If yours doesn't support that — or you'd rather enter the settings by hand — the exact IMAP, POP3, and SMTP hostnames are listed on the Email page: click **Setup Instructions → View Instructions** under **Mail Server Access** for a step-by-step that includes the right hostnames, ports, and security settings for your server. Most apps configure themselves from the records above once you enter your address and password. If yours doesn't support that — or you'd rather enter the settings by hand — the exact IMAP, POP3, and SMTP hostnames are listed on the Email page: click **Setup Instructions** at the top of the page for a step-by-step that includes the right hostnames, ports, and security settings for your server.
The typical settings look like this; substitute the hostname shown in the Setup Instructions: The typical settings look like this; substitute the hostname shown in the Setup Instructions:
@@ -82,33 +82,35 @@ IMAP (incoming)
SMTP (outgoing) SMTP (outgoing)
Host: <see Setup Instructions> Host: <see Setup Instructions>
Port: 587 Port: 465
Security: STARTTLS Security: SSL/TLS
Username: full email address Username: full email address
Password: same as IMAP Password: same as IMAP
``` ```
For outgoing mail, **port 465 with SSL/TLS** is the standard. If your client prefers STARTTLS, **port 587** is the alternate submission port. (Don't use port 25 from a mail client — it's for server-to-server delivery and most networks block it.)
For per-client walkthroughs (Outlook, Apple Mail, Thunderbird, etc.), see the Email clients section — coming soon. For per-client walkthroughs (Outlook, Apple Mail, Thunderbird, etc.), see the Email clients section — coming soon.
## Webmail ## Webmail
Click **Webmail Access → Open Webmail** on the Email page to sign in to webmail in a new tab. Click **Webmail** at the top of the Email page to sign in to webmail in a new tab.
## Verify it worked ## Verify it worked
Send yourself a test message from another account (your personal Gmail, for example). It should arrive within a minute or two and be retrievable from both your client and webmail. Send yourself a test message from another account (your personal Gmail, for example). It should arrive within a minute or two and be retrievable from both your client and webmail.
<Aside type="caution"> <Aside type="caution">
**SPF and DKIM records matter.** Without them, your outgoing mail will get flagged or rejected by other providers. We add an SPF record automatically when you add a domain. DKIM records are listed in the **DKIM Records** section near the bottom of the Email page — make sure they're present at your registrar if the domain isn't using our nameservers. **SPF and DKIM records matter.** Without them, your outgoing mail will get flagged or rejected by other providers. We add an SPF record automatically when you add a domain. DKIM records are listed in the **DKIM Management** section on the **Email Domains (DNS)** tab — make sure they're present at your registrar if the domain isn't using our nameservers.
</Aside> </Aside>
## Troubleshooting ## Troubleshooting
**Webmail isn't reachable.** DNS for the mail subdomain may still be propagating — wait an hour and try again. **Webmail isn't reachable.** Webmail is hosted at our address — the **Webmail** button on the Email page opens it directly — so it doesn't depend on your domain or its DNS. If it doesn't load, it's almost always a temporary connection issue: try again in a few minutes or from another network, and open a support ticket if it persists.
**Outgoing mail is bouncing or going to spam.** Check the SPF and DKIM records. The DKIM Records panel on the Email page shows whether DKIM is configured for each of your domains. **Outgoing mail is bouncing or going to spam.** Check the SPF and DKIM records. The **DKIM Management** section on the **Email Domains (DNS)** tab shows whether DKIM is configured for each of your domains.
**Client can connect on IMAP but not SMTP.** Some ISPs and corporate networks block outgoing port 587. Try sending from a different network to confirm; if the issue is your network, your ISP is the place to ask. **Client can connect on IMAP but not SMTP.** Some ISPs and corporate networks block outgoing mail ports. If sending fails on port 465, try the alternate submission port **587** (STARTTLS); if both fail, test from a different network to confirm it's your network, and if so, your ISP is the place to ask.
## Related ## Related
+26 -3
View File
@@ -41,10 +41,33 @@ Outputs one PNG per entry in `shots.config.ts` to `src/assets/screenshots/whp/<i
## Refresh workflow ## Refresh workflow
UI changed? → `npm run screenshots` locally → review the diffs (`git diff --stat` shows changed PNGs) → eyeball them for accidental leakage → commit → push. UI changed? → run the capture (see below) locally → review the diffs (`git diff --stat` shows changed PNGs) → **open each changed PNG and eyeball it for accidental leakage** (server hostname, IP, account ID, customer domains/usernames) → commit → push.
- **A page covered by `shots.config.ts`** (a plain navigate-and-shoot page): `npm run screenshots`.
- **A page that needs interaction** — opening a modal, ticking checkboxes, switching tabs — lives in a **section capture script** (see below). Re-run that script instead.
When a section is *reworked* (not just restyled), also: re-walk the new UI to find every state worth a screenshot, update the section script's steps, add/rename the `whp-<section>-*` ids, then refresh the `.mdx` references and run `npm run build` to confirm links and images resolve.
## Section capture scripts
`shots.config.ts` + `run.ts` only do navigate → redact → screenshot. Anything that needs **interaction or per-section redaction** gets its own `capture-<section>.ts`, run directly with `tsx`:
```bash
set -a; source tools/screenshots/.env; set +a
npx tsx tools/screenshots/capture-dns.ts
```
| Script | Covers | Auth |
| --- | --- | --- |
| `capture-admin.ts` | Server Settings tabs, admin pages | `WHP_ADMIN_USER` |
| `capture-site-builder.ts` | Site Builder editor states | `WHP_USER` |
| `capture-dns.ts` | Domains & DNS list, Add Domain modal, records editor, bulk toolbar | `WHP_USER` |
| `capture-email.ts` | Email page "Mail Client Setup" section (autodiscovery DNS records) | `WHP_USER` |
Each script carries its own `redact()` (text-node + input-value swaps) so fleet hostnames, IPs, and customer data become neutral placeholders while brand/demo domains stay visible. Copy the closest existing script when adding a new section — match its viewport (1440×900), `deviceScaleFactor: 2`, and **read-only** discipline (open modals and tick boxes for the shot, but never save/delete/submit).
## Adding a new shot ## Adding a new shot
1. Add an entry to `shots.config.ts` with a stable `id`. 1. **Static page?** Add an entry to `shots.config.ts` with a stable `id`, then `npm run screenshots`.
2. `npm run screenshots`. 2. **Interactive state?** Add the step to the relevant `capture-<section>.ts` (or copy one for a new section), then `npx tsx tools/screenshots/capture-<section>.ts`.
3. Reference the new file in your `.mdx`: `![Alt text](~/assets/screenshots/whp/<id>.png)`. 3. Reference the new file in your `.mdx`: `![Alt text](~/assets/screenshots/whp/<id>.png)`.
+38 -20
View File
@@ -1,16 +1,21 @@
/** /**
* Email capture — the customer "Mail Client Setup" section on the Email page. * Email capture — the tabbed Email Management page, as the demo customer.
* *
* Captures, as the demo customer: * Captures:
* - whp-email-autodiscovery.png the Mail Client Setup accordion section, * - whp-email.png the Email page on its default "Email Accounts"
* expanded, with the per-domain autodiscovery * tab: the top button strip (Webmail / Admin
* DNS records table + copyable zone block. * 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), redacted for our multi-server fleet: the mail-server * Viewport-only (1440x900, deviceScaleFactor 2), redacted for our multi-server
* hostname becomes a neutral placeholder, while the brand demo domain * fleet: server/mail hostnames + IPs become placeholders, while the brand demo
* (whp-demo.anhh.co) is kept visible on purpose. * domain (whp-demo.anhh.co) is kept visible on purpose.
* *
* Read-only: expands an accordion section for the screenshot, never saves. * Read-only: switches tabs / selects a domain for the shot, never saves.
*/ */
import { chromium, type Page } from 'playwright'; import { chromium, type Page } from 'playwright';
import { mkdir } from 'node:fs/promises'; import { mkdir } from 'node:fs/promises';
@@ -42,8 +47,9 @@ async function login(page: Page) {
/** /**
* Neutralise fleet-identifying text before the screenshot. The brand demo * Neutralise fleet-identifying text before the screenshot. The brand demo
* domain (anhh.co) is intentionally preserved; the mail-server host and any * domain (anhh.co) is intentionally preserved; mail/server hosts and IPs are
* server hostnames/IPs are swapped for placeholders. * 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) { async function redact(page: Page) {
await page.addStyleTag({ content: HIDE_CSS }); await page.addStyleTag({ content: HIDE_CSS });
@@ -79,24 +85,36 @@ async function main() {
try { try {
await login(page); await login(page);
await page.goto(`${BASE}/index.php?page=email-management`, { waitUntil: 'networkidle' }); await page.goto(`${BASE}/index.php?page=email-management`, { waitUntil: 'networkidle' });
await page.waitForSelector('#email-mgmt-nav', { state: 'visible' });
// Expand the (collapsed-by-default) "Mail Client Setup" accordion section. // --- Shot 1: the default Email Accounts tab (orientation) ---
await page.locator('button[data-bs-target="#mail-client-setup"]').click(); 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' }); await page.waitForSelector('#custMailDnsDomain', { state: 'visible' });
// The zone <pre> is populated on DOMContentLoaded; re-run to be safe. // Ensure a domain is selected, then (re)render the zone block.
await page.evaluate(() => { 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; const fn = (window as unknown as { renderCustMailDns?: () => void }).renderCustMailDns;
if (typeof fn === 'function') fn(); if (typeof fn === 'function') fn();
}); });
await page.waitForTimeout(500); await page.waitForTimeout(500);
await redact(page); // re-run: tab content rendered after the first pass
await redact(page); // The autodiscovery section is the .card wrapping the domain <select>.
const item = page.locator('#mail-client-setup').locator('xpath=ancestor::div[contains(@class,"accordion-item")]'); const card = page.locator('#custMailDnsDomain').locator('xpath=ancestor::div[contains(@class,"card")][1]');
await item.scrollIntoViewIfNeeded(); await card.scrollIntoViewIfNeeded();
await page.waitForTimeout(300); await page.waitForTimeout(300);
const path = resolve(OUT_DIR, 'whp-email-autodiscovery.png'); const autoPath = resolve(OUT_DIR, 'whp-email-autodiscovery.png');
await item.screenshot({ path }); await card.screenshot({ path: autoPath });
console.log(`captured whp-email-autodiscovery -> ${path}`); console.log(`captured whp-email-autodiscovery -> ${autoPath}`);
} finally { } finally {
await browser.close(); await browser.close();
} }
+116
View File
@@ -0,0 +1,116 @@
/**
* Traffic analytics capture — for the June 2026 platform-updates blog post.
*
* Captures, as the demo customer:
* - traffic-analytics-overview.png View Traffic landing: Yesterday's Snapshot,
* Top URLs / Bandwidth Consumers, Daily Totals
* - traffic-analytics-day-detail.png the per-day drill-down: hourly request graph,
* top pages by views, top pages by bandwidth
*
* Viewport 1440x900, deviceScaleFactor 2, fullPage:false. Redacts the fleet
* server strip ("WHP-01" / "Welcome, ...") and the version-number footer; keeps
* the brand demo domain (whp-demo.anhh.co) visible on purpose. Read-only.
*
* Output goes to /workspace/blog-assets (this is a blog image, not a KB page).
*/
import { chromium, type Page } from 'playwright';
import { mkdir } from 'node:fs/promises';
import { resolve } from 'node:path';
const OUT_DIR = '/workspace/blog-assets';
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');
// Hide the server-identifying navbar strip and the version footer.
const HIDE_CSS = `.navbar-text, .brand-full, .navbar-brand { 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][] = [
[/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>'],
[/demo-user/g, 'your-username'],
// Strip the release/version identifier from the footer.
[/Web Hosting Panel\s*-\s*\d{4}\.\d{2}\.\d+/gi, 'Web Hosting Panel'],
[/\b\d{4}\.\d{2}\.\d+\b/g, ''],
];
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);
// Resolve the demo site's traffic view via the "View Traffic" button.
await page.goto(`${BASE}/index.php?page=site-traffic`, { waitUntil: 'networkidle' });
await page.locator('a:has-text("View Traffic"), button:has-text("View Traffic")').first().click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
const trafficUrl = page.url();
// 1. Overview — clip to the main content card so the snapshot + daily totals
// frame nicely without the empty side gutters.
await redact(page);
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(300);
await page.screenshot({ path: resolve(OUT_DIR, 'traffic-analytics-overview.png'), fullPage: false });
console.log('captured traffic-analytics-overview');
// 2. Day drill-down — click the 23rd (richest demo data), then clip to the
// "Breakdown for ..." card (hourly graph + top pages + bandwidth).
await page.locator('a:has-text("2026-06-23")').first().click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1500);
await redact(page);
const card = page.locator('#day-detail, .card:has-text("Breakdown for")').first();
await card.scrollIntoViewIfNeeded().catch(() => {});
await page.waitForTimeout(400);
if (await card.count()) {
await card.screenshot({ path: resolve(OUT_DIR, 'traffic-analytics-day-detail.png') });
} else {
await page.screenshot({ path: resolve(OUT_DIR, 'traffic-analytics-day-detail.png'), fullPage: false });
}
console.log('captured traffic-analytics-day-detail');
console.log('traffic url was', trafficUrl);
} finally {
await browser.close();
}
}
main().catch((err) => { console.error(err); process.exit(1); });