to add a click-to-zoom lightbox
+ // (medium-zoom) that targets article content images.
+ Head: './src/components/Head.astro',
},
customCss: [
'@fontsource-variable/inter',
@@ -60,7 +63,7 @@ export default defineConfig({
},
{
label: 'Admin',
- badge: { text: 'Draft', variant: 'caution' },
+ // badge removed once content was verified against the real UI
items: [{ autogenerate: { directory: 'whp/admin' } }],
},
],
diff --git a/package-lock.json b/package-lock.json
index 3eefe66..6dc891d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"astro": "^6.3.1",
+ "medium-zoom": "^1.1.0",
"sharp": "^0.34.5"
},
"devDependencies": {
@@ -4359,6 +4360,12 @@
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
"license": "CC0-1.0"
},
+ "node_modules/medium-zoom": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/medium-zoom/-/medium-zoom-1.1.0.tgz",
+ "integrity": "sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==",
+ "license": "MIT"
+ },
"node_modules/micromark": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
diff --git a/package.json b/package.json
index 8652035..fe6b242 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"astro": "^6.3.1",
+ "medium-zoom": "^1.1.0",
"sharp": "^0.34.5"
},
"devDependencies": {
diff --git a/src/assets/screenshots/whp/admin-coraza.png b/src/assets/screenshots/whp/admin-coraza.png
new file mode 100644
index 0000000..e10580c
Binary files /dev/null and b/src/assets/screenshots/whp/admin-coraza.png differ
diff --git a/src/assets/screenshots/whp/admin-ignore-rules.png b/src/assets/screenshots/whp/admin-ignore-rules.png
new file mode 100644
index 0000000..1521cf5
Binary files /dev/null and b/src/assets/screenshots/whp/admin-ignore-rules.png differ
diff --git a/src/assets/screenshots/whp/admin-issues.png b/src/assets/screenshots/whp/admin-issues.png
new file mode 100644
index 0000000..46de95e
Binary files /dev/null and b/src/assets/screenshots/whp/admin-issues.png differ
diff --git a/src/assets/screenshots/whp/admin-monitor-admin.png b/src/assets/screenshots/whp/admin-monitor-admin.png
new file mode 100644
index 0000000..35247e6
Binary files /dev/null and b/src/assets/screenshots/whp/admin-monitor-admin.png differ
diff --git a/src/assets/screenshots/whp/admin-srvset-system.png b/src/assets/screenshots/whp/admin-srvset-system.png
new file mode 100644
index 0000000..3da7636
Binary files /dev/null and b/src/assets/screenshots/whp/admin-srvset-system.png differ
diff --git a/src/assets/screenshots/whp/admin-user-mgmt.png b/src/assets/screenshots/whp/admin-user-mgmt.png
new file mode 100644
index 0000000..3bda7d3
Binary files /dev/null and b/src/assets/screenshots/whp/admin-user-mgmt.png differ
diff --git a/src/assets/screenshots/whp/admin-user-resources.png b/src/assets/screenshots/whp/admin-user-resources.png
new file mode 100644
index 0000000..160af0a
Binary files /dev/null and b/src/assets/screenshots/whp/admin-user-resources.png differ
diff --git a/src/components/Head.astro b/src/components/Head.astro
new file mode 100644
index 0000000..d6d1f94
--- /dev/null
+++ b/src/components/Head.astro
@@ -0,0 +1,34 @@
+---
+import Default from '@astrojs/starlight/components/Head.astro';
+---
+
+
+
+
diff --git a/src/content/docs/whp/admin/backups.mdx b/src/content/docs/whp/admin/backups.mdx
new file mode 100644
index 0000000..cb78c91
--- /dev/null
+++ b/src/content/docs/whp/admin/backups.mdx
@@ -0,0 +1,120 @@
+---
+title: Backups
+description: How WHP's automatic backups work, the default-target requirement, full-server backups vs customer data backups, and managing backup targets.
+sidebar:
+ order: 6
+---
+
+import { Aside } from '@astrojs/starlight/components';
+import SuperAdmin from '~/content/partials/super-admin-callout.mdx';
+import AdminSignIn from '~/content/partials/admin-signin.mdx';
+import Support from '~/content/partials/support-link.mdx';
+
+
+
+WHP backs up **customer data** (sites and databases) when an admin has configured a default backup target. WHP does **not** back up the full server — that's a separate concern.
+
+## Two different things called "backup"
+
+It's worth being precise:
+
+- **Customer data backups** — what WHP does. Site files (per user, per domain) and databases. Configured per server via backup targets. These start automatically once a default target exists.
+- **Full server backups** — backing up the host OS, `/etc`, container images, the WHP install itself, etc. **WHP does not do this.**
+
+### Full server backups
+
+| Where WHP runs | Who owns full-server backups |
+|---|---|
+| **Our Virtual Dedicated Server (VDS)** plans | Included. AnHonestHost snapshots the VDS at the platform level. |
+| **Anywhere else** | The server operator. WHP doesn't ship a full-server backup mechanism — you'll need to set up something at the OS / hypervisor level. |
+
+If you're running WHP on your own infrastructure, plan accordingly. WHP's configuration isn't all in `/etc` — there are config and state files in the WHP install directory, in Docker volumes for the platform containers (HAProxy, MySQL, Postgres, Valkey, the WAF), and in service-specific paths elsewhere on the host. The safest approach is a **full-server backup** (image snapshot or filesystem-level backup) rather than trying to enumerate paths.
+
+## Sign in as super admin
+
+
+
+## How customer auto-backups start
+
+**Customer auto-backups don't run until at least one default backup target exists.** A fresh WHP install has no targets and no schedule — every user's backup status is "no targets available" until an admin sets one up.
+
+Once a default target is configured, the platform begins automatic daily backups for every customer with sites or databases. Customers see their own backup history under the [Backups](/whp/how-to/backups/) page in their account.
+
+## The Backup Management page
+
+Sidebar → **Backups**. The admin view of this page mirrors the customer view, with extra controls:
+
+- **Stat tiles** — Total Backups, Total Size, Sites, Databases. Server-wide totals across every account.
+- **Create New Backup** — fires an on-demand backup. Admin form adds a **User** dropdown so you can backup any customer's account, not just your own.
+- **Backup Targets** — the table of destinations available on the server. The **Global** column distinguishes shared targets from per-account targets.
+
+### Backup targets
+
+Each target row has:
+
+- **Name** — your label for the destination.
+- **Type** — S3 (and other supported types) — built-in support for S3-compatible storage (AWS S3, Cloudflare R2, MinIO, etc.).
+- **Connection** — the endpoint URL and bucket / path.
+- **Retention** — how long backups are kept (default 5 days).
+- **Max Backups** — cap on the number of snapshots retained (default 10).
+- **Global** — `Yes` if every account can use this target, `No` if it's bound to a single account.
+- **Actions** — **Test** (verify credentials and write a probe object), **Edit**, **Delete**.
+
+### Adding a global backup target
+
+Click **+ Add Backup Target** and fill in:
+
+1. **Name** — descriptive label.
+2. **Type** — pick S3 (or whichever storage backend you want).
+3. **Endpoint URL** — for non-AWS S3 (Cloudflare R2, MinIO, Wasabi, etc.), point at the provider's endpoint.
+4. **Bucket / path**.
+5. **Access key / Secret** — the credentials WHP will use.
+6. **Retention / Max Backups** — server-wide defaults for any account that uses this target.
+7. **Global** — leave **on** so every account can use it as their default destination.
+
+Click **Test** before saving to confirm WHP can reach the bucket. A good target round-trips a probe object in under a second.
+
+
+
+### Per-customer (non-global) targets
+
+Customers can add their own backup targets from the customer-side Backups page. Those show up here with **Global: No** plus a note linking them to the owning account. Admins can edit or delete those on the customer's behalf when they need help.
+
+### Triggering an on-demand backup for a user
+
+In the **Create New Backup** form:
+
+1. Pick the customer in the **User** dropdown.
+2. Pick a **Backup Type** (Sites / Databases / both).
+3. Pick a **Backup Target**.
+4. **Start Backup**. Progress is visible in the run history below.
+
+This is the right path when a customer asks for a fresh backup right before a risky migration.
+
+## Verifying customer backups are succeeding
+
+The Total Backups and Total Size tiles climb over time on a healthy server. If they sit flat:
+
+- Confirm at least one target exists with **Global: Yes**.
+- Open the run history (lower on the page) and look for failed entries — the error message usually points at credentials or quota.
+
+## Troubleshooting
+
+**No backups are running for any customer.** Confirm at least one **Global: Yes** target exists and that its **Test** button returns success. Without a global default, the scheduler doesn't fire.
+
+**One target is failing.** Click **Test** on the target row. The most common causes are rotated credentials, an incorrect endpoint URL (R2 / MinIO often need an explicit endpoint different from AWS's default), or the bucket lifecycle policy deleting backups before the retention window.
+
+**Customer says their backup is too old.** Check **Max Backups** on the target — if it's lower than their backup cadence × retention window, older backups get pruned.
+
+**Backup ran but `tar` step failed mid-stream.** Disk pressure on the host is a common cause. Check **Disk Usage** (admin sidebar) and consider raising the target's retention so fewer backups stack up on the host before upload.
+
+## Related
+
+- [Backups](/whp/how-to/backups/) — the customer-facing side.
+- [Server settings & services](/whp/admin/server-settings/) — including Backup Upload host service.
+
+## Still stuck?
+
+
diff --git a/src/content/docs/whp/admin/coraza-waf.mdx b/src/content/docs/whp/admin/coraza-waf.mdx
index 85a0016..b5d0d6a 100644
--- a/src/content/docs/whp/admin/coraza-waf.mdx
+++ b/src/content/docs/whp/admin/coraza-waf.mdx
@@ -1,95 +1,108 @@
---
title: Coraza WAF rules
-description: Tune the Coraza web-application firewall rules running in front of your sites — toggle modes, mute false positives, audit blocks.
+description: Set the global WAF mode, tune individual rules, audit blocked requests, and add per-host overrides.
sidebar:
order: 3
- badge:
- text: Draft
- variant: caution
---
import { Aside } from '@astrojs/starlight/components';
import SuperAdmin from '~/content/partials/super-admin-callout.mdx';
-import Draft from '~/content/partials/draft-callout.mdx';
-import SignIn from '~/content/partials/signing-in.mdx';
+import AdminSignIn from '~/content/partials/admin-signin.mdx';
import Support from '~/content/partials/support-link.mdx';
-
+[Coraza](https://coraza.io/) is an open-source web-application firewall (WAF) that runs as a sidecar in front of your sites. The **Coraza Rules** page in the admin sidebar gives you a UI to set the global mode, tune individual rules, and audit blocked requests — all without rebuilding the sidecar image.
-[Coraza](https://coraza.io/) is an open-source web-application firewall (WAF). It runs as a sidecar in front of your sites and inspects incoming requests against rule families like OWASP Core Rule Set v4 (CRS). The admin WHP gives you a UI to manage the rules and audit what's been blocked.
+
-## Three operating modes
+## Global WAF mode
-The WAF runs in one of three modes, set per-site or server-wide:
+A coloured pill at the top of the page shows the **Global WAF mode** with a **change** link:
-- **Off.** No inspection. Requests pass through untouched.
-- **Detect-only.** Inspect every request and log matches, but pass them through. Use this when rolling out the WAF for the first time or when validating a rule change.
-- **Enforce.** Inspect every request and **block** any that match an enforcing rule. This is the production setting once you've validated detect-only.
+- **Off** — no inspection. Requests pass untouched.
+- **Detect** — inspect and log matches; do not block. Use during a roll-out or while validating a rule change.
+- **Enforce** — inspect and block anything that matches an enforcing rule.
-The WAF is fail-open: if the Coraza sidecar itself is unhealthy, traffic still flows.
+The WAF is **fail-open**: if the Coraza sidecar is itself unhealthy, traffic still flows.
-## Sign in to WHP
+## Sign in as super admin
-
+
-## Where it lives
+## The three tabs
-Sidebar → **Security → Coraza Rules**. The page lists rule families (CRS 901, 911, 913, 920–922, 930–934, 941–944, 949, 950–956, 959, 980) and per-rule controls.
+### Firing rules
+
+The default view. Each row is a rule that has matched at least one request in the selected time window (toggle **Last 24h** or **Last 7d** at top right).
+
+Columns:
+
+- **Rule ID** — the CRS or custom rule identifier. **view** opens the rule definition. **Ask AI** opens an explanation of what this rule catches.
+- **Hits** — how many times the rule fired in the time window.
+- **Top hosts** — the top customer domains that triggered this rule (with per-host hit counts).
+- **Top URIs** — the most common request paths that triggered it (helpful for distinguishing scans from real traffic).
+- **State** — per-rule override of the global mode. Options: `(default → enforce)`, `(default → detect)`, `(default → score)`, `off`. Picking anything other than `(default → …)` overrides the global mode for that rule.
+- **Per-host** — opens a **Hosts (N)** drawer to set per-host overrides for that rule.
+
+
+
+### CRS catalog
+
+The full OWASP Core Rule Set catalogue (v4 families: 901, 905, 911, 913, 920–922, 930–934, 941–944, 949, 950–956, 959, 980). Use this when you want to look up a rule that *hasn't* fired yet — for example, to pre-mute a rule you know will produce false positives on a specific app.
+
+### Activity
+
+A timeline / log of every WAF block in the audit window. Use it to:
+
+- Confirm a customer report (cross-reference the **X-Request-Reference** UUID on the visitor-facing 403 page).
+- Spot bursts of activity from the same source IP.
+- Tune source-of-truth back-end queries — the audit is read from `security.db`.
## Common tasks
### Roll a new site onto the WAF
-
-
-1. Open **Security → Coraza Rules**.
-2. Find the site and set mode to **Detect-only**.
-3. Drive normal traffic for at least 24 hours.
-4. Open the **Audit log** and filter to that site. Confirm no legitimate request is matching an enforcing rule.
-5. Switch the site to **Enforce**.
+1. Set the rule's **State** to `(default → detect)` for that site via the Per-host drawer.
+2. Drive normal traffic for at least 24 hours.
+3. Open the **Activity** tab and filter to that host. Confirm no legitimate request is matching an enforcing rule.
+4. Flip the host to **enforce**.
### Mute a noisy rule
-When a rule is firing on legitimate traffic for one site:
+1. Find the rule in **Firing rules**.
+2. Click **Per-host → Hosts (N)** to add a per-site mute, OR change the rule's **State** column to mute it globally.
-1. Click the audit-log row to see the rule_id and the matched request.
-2. From the **Coraza Rules** page, find the rule by ID.
-3. Pick **Ignore for this site** (per-site mute) or **Ignore globally** (server-wide mute).
-4. Save. The rule stops firing on the next request.
-
-Per-site is almost always the right scope. Use global mute sparingly — it weakens the WAF for every site.
+Prefer per-host. Global mute weakens the WAF for every site.
### Audit a block
-Customer reports a request was wrongly blocked? The branded 403 page that visitors see includes an **X-Request-Reference** UUID. Cross-reference it:
+Customer reports a request was wrongly blocked? The branded 403 page includes an **X-Request-Reference** UUID. Cross-reference it:
-1. In the **Audit log**, search for the UUID.
-2. The audit row shows the matched rule_id, the source IP, the URL, and the offending parameter.
+1. In the **Activity** tab, search for the UUID.
+2. The row shows the matched rule ID, source IP, URL, and offending parameter.
3. Decide whether to mute the rule (see above) or leave it — many "false positives" turn out to be real attempts.
-## Things to know
+## Implementation notes
-- **Rule changes apply on the next request.** No service restart needed for tuning.
-- **Adding or removing rules requires a full reload** of `coraza-spoa`, not just a SIGHUP. The panel handles this for you; if you edit rule files by hand, `docker restart coraza-spoa`.
-- **Real source IPs are in the audit log.** Even though haproxy fronts the WAF, we propagate the real client IP through the SPOE messages.
-- **`SecRuleRemoveById` plus a new rule needs a full restart**, not just a config reload. Again, the panel handles this when you change rules through the UI.
+- **Source of truth: `security.db`.** All rule edits go through the panel and write to that SQLite file; manual file edits won't survive a regeneration.
+- **Reload mechanism.** Rule edits apply via SIGHUP to `coraza-spoa`; adding or removing whole rule files requires a full container restart, which the panel performs when the change needs it.
+- **Real source IPs are in the audit log** — even with HAProxy in front of the WAF, the real client IP is propagated through the SPOE messages.
## Troubleshooting
-**A rule shows enabled but doesn't fire.** Check that the site is in **Detect-only** or **Enforce** mode. A site in **Off** mode bypasses every rule, including enabled ones.
+**A rule shows enabled but doesn't fire.** Check that the host is in **detect** or **enforce** mode. A host in **off** bypasses every rule.
-**The audit log is empty.** Confirm `coraza-spoa` is healthy on the **Services** page. If it's restarting in a loop, check the container logs — most often a malformed rule file or a missing include.
+**The Activity log is empty.** Confirm `coraza-spoa` is healthy on the **Services** tab of Server Settings. If it's restarting in a loop, check the container logs — usually a malformed rule file or a missing include.
-**Edits revert on restart.** Make sure you're editing through the panel; manual edits to files outside the panel-managed path are overwritten by config regeneration.
+**Edits revert on restart.** Make sure you're editing through the panel; manual edits outside the panel-managed path are overwritten.
## Related
- [Server settings & services](/whp/admin/server-settings/)
-- [Site Monitoring rules](/whp/admin/site-monitoring/)
+- [AI Monitor, Issues & Ignore Rules](/whp/admin/site-monitoring/)
## Still stuck?
diff --git a/src/content/docs/whp/admin/overview.mdx b/src/content/docs/whp/admin/overview.mdx
index 652f847..8231600 100644
--- a/src/content/docs/whp/admin/overview.mdx
+++ b/src/content/docs/whp/admin/overview.mdx
@@ -1,43 +1,67 @@
---
title: Admin overview
-description: What WHP super admin unlocks — server-wide controls for services, security rules, monitoring, and users.
+description: What WHP super admin unlocks — server-wide controls for services, mail, DNS, security, monitoring, and users.
sidebar:
order: 1
- badge:
- text: Draft
- variant: caution
---
import SuperAdmin from '~/content/partials/super-admin-callout.mdx';
-import Draft from '~/content/partials/draft-callout.mdx';
import Support from '~/content/partials/support-link.mdx';
-
-
## What super admin unlocks
-The same WHP panel you'd use on a customer account scales up: customers with **super admin** access also see server-wide pages for managing services, firewall rules, monitoring policy, and users. The customer-facing sections (Sites, Domains, Email, etc.) work the same way; the admin sections sit alongside them, gated to admins.
+WHP's super admin role exposes server-wide pages alongside the customer-facing nav. The customer pages (Sites, Domains, Email, etc.) work identically for everyone; the admin pages sit alongside them and are gated to the super admin only.
-Today, super admin is most commonly handed to customers running a [Virtual Dedicated Server](https://anhonesthost.com/vds) — they get full server control as part of the plan.
+**Today, the only super admin is the `root` user on the server.** There's no UI to add another super admin — if you need additional people to have super admin, share the root credentials via your usual secret-sharing flow (or [open a ticket](https://secure.anhonesthost.com/submitticket.php) if you need a different model).
+
+This typically applies to customers running a [Virtual Dedicated Server](https://anhonesthost.com/vds) — they get full server control as part of the plan and sign in to WHP as `root`.
+
+### Signing in as super admin
+
+import AdminSignIn from '~/content/partials/admin-signin.mdx';
+
+
+
+## Admin-only sidebar sections
+
+When you sign in as a super admin, these sections appear in addition to the customer-facing nav:
+
+- **AI Monitor** — the admin dashboard, plus **Issues**, **Site Reports**, and **Ignore Rules**.
+- **Security Management** — security policy across the server.
+- **Coraza Rules** — Web-application firewall (WAF) rule tuning, audit, and global mode.
+- **User Management** — create WHP users, set account types, manage existing users.
+- **User Resources** — per-user CPU/RAM/disk allowances and current usage.
+- **Delegated Users** — list of contractor / sub-account access grants.
+- **Active Sessions** — every signed-in browser across the server.
+- **Server Settings** — System, Services, Mail, DNS, Network & SSL, Security tabs.
+- **Disk Usage** — server-wide disk consumption breakdown.
+- **Announcements Management** — edit the announcements that appear on every customer's dashboard.
+- **Update Management** — apply WHP platform updates.
+- **Docker Management** — see and manage every container on the host.
+- **Valkey Admin** — server-wide Valkey configuration.
+- **Container Boot & Health** — boot-order and per-container health.
+- **Site Disable Audit** — record of sites disabled / re-enabled.
+- **Account Suspensions** — suspended customer accounts.
## What's in this section
-- **[Server settings & services](/whp/admin/server-settings/)** — restart services, manage modules and runtimes, edit server-wide configuration.
-- **[Coraza WAF rules](/whp/admin/coraza-waf/)** — view, tune, and tune out web-application-firewall rules across all your sites.
-- **[Site Monitoring rules](/whp/admin/site-monitoring/)** — manage the rules that drive Site Monitoring alerts.
-- **[Users & delegated access](/whp/admin/user-management/)** — create sub-users, delegate panel access, manage SFTP/SSH users.
+- **[Server settings & services](/whp/admin/server-settings/)** — the six tabs under Server Settings: system info, restart services, mail-server config, DNS / nameservers, HAProxy + SSL, and integration API keys.
+- **[Coraza WAF rules](/whp/admin/coraza-waf/)** — set the global WAF mode, tune individual rules, and audit blocked requests.
+- **[AI Monitor, Issues & Ignore Rules](/whp/admin/site-monitoring/)** — the three pages that drive the Site Monitoring add-on.
+- **[Users & delegated access](/whp/admin/user-management/)** — create accounts, set account types, delegate access, and handle suspensions.
+- **[Backups](/whp/admin/backups/)** — configure the default backup target so customer auto-backups start running. Full-server backups are a separate, plan-dependent concern.
## Things to know before you change server-wide settings
- **One change can affect every site on the server.** Where customer-side pages scope changes to one site, admin pages typically scope to the whole server.
- **Service restarts are visible to live traffic.** Restart Apache or PHP-FPM during a quiet window when possible.
-- **Backups still apply.** Server-level changes don't bypass the [backups](/whp/how-to/backups/) you have configured; you can roll back the data side, but service-config changes you made by hand aren't snapshotted unless you back up `/etc` somewhere on your own.
+- **Customer backups don't cover server config.** The [customer backups](/whp/how-to/backups/) you've configured snapshot site files and databases, not server-level changes you make by hand. Server config lives in several places — not just `/etc` — so the right safety net is a **full-server backup**. On AnHonestHost-managed plans and VDS we handle that for you; running WHP elsewhere, the operator is responsible. See [Backups](/whp/admin/backups/) for the full picture.
## Related
-- [What is containerized hosting?](/whp/getting-started/what-is-containerized-hosting/) — the differences between container plans and full server access.
+- [What is containerized hosting?](/whp/getting-started/what-is-containerized-hosting/) — differences between container plans and full server access.
## Still stuck?
diff --git a/src/content/docs/whp/admin/server-settings.mdx b/src/content/docs/whp/admin/server-settings.mdx
index e5df25d..e2cb0bf 100644
--- a/src/content/docs/whp/admin/server-settings.mdx
+++ b/src/content/docs/whp/admin/server-settings.mdx
@@ -1,74 +1,115 @@
---
title: Server settings & services
-description: Restart Apache, PHP-FPM, MySQL; manage PHP modules and runtimes; edit server-wide configuration.
+description: Restart services, configure mail server, manage DNS / nameservers, HAProxy + system SSL certificates, and integration API keys.
sidebar:
order: 2
- badge:
- text: Draft
- variant: caution
---
import { Aside } from '@astrojs/starlight/components';
import SuperAdmin from '~/content/partials/super-admin-callout.mdx';
-import Draft from '~/content/partials/draft-callout.mdx';
-import SignIn from '~/content/partials/signing-in.mdx';
+import AdminSignIn from '~/content/partials/admin-signin.mdx';
import Support from '~/content/partials/support-link.mdx';
-
+The **Server Settings** page lives in the admin sidebar and has six tabs along the left rail. Each tab is a different surface area of the server you can inspect or change.
-This page covers the server-wide controls available in the WHP admin sections — restarting services, managing PHP modules and versions, and adjusting server-level configuration.
+
-## Sign in to WHP
+## Sign in as super admin
-
+
-## Restarting services
+## The six tabs
-The admin **Services** page lists the long-running services that run on the server: Apache (front-end web), PHP-FPM (one or more pools), MySQL or MariaDB, the mail stack, and any add-on services like Valkey or PostgreSQL if you've enabled them.
+### System
-Each service has a status indicator and **Restart**, **Stop**, and **Start** controls. Restart is the safe default for picking up new configuration.
+Read-only system summary plus two simple change controls:
+
+- **System Information** — Hostname, Operating System, Kernel, Timezone, Uptime, Load Average, Disk Usage, Memory Usage.
+- **Hostname Settings** — change the server's FQDN. Restart the relevant services after a hostname change.
+- **Timezone Settings** — change the system timezone (affects cron timing, backup schedules, log timestamps).
-## Managing PHP modules and runtimes
+### Services
-WHP can run multiple PHP runtimes side-by-side (PHP 8.3, 8.4, etc.). With super admin you can:
+Service status and restart controls.
-- Install additional PHP runtimes via the **PHP Versions** admin page.
-- Add or remove extensions per runtime — common ones (mbstring, intl, opcache, imagick, redis, etc.) are toggles; less common ones may require a [support ticket](https://secure.anhonesthost.com/submitticket.php).
-- Edit a runtime's `php.ini` from the **PHP Configuration** sub-page, then reload PHP-FPM to pick it up.
+- **Service Status** — health pills for the host-side services:
+ - **Apache** and **PHP-FPM** on the host serve the **WHP control panel itself**, not customer sites. Customer sites run inside their own per-site containers, separate from these host services.
+ - **Docker** — the host's Docker daemon. If this is down, no customer container will start.
+ - **ProFTPD** — host FTP service (used by FTP-enabled customer accounts).
+ - **Backup Upload** — the host-side uploader that streams backups to your configured backup targets.
+- **Restart Services** — checkboxes per service plus **Restart Selected Services**.
+- **Docker Container Management** — status of the core platform containers (Mysql, Haproxy manager, Memcache, Postgresql) and a per-container **Execute Operation** picker (e.g. restart a single container).
-Switching a site to a different PHP version is done on the customer side — open **Sites → your site** and pick from the **Container Type / PHP Version** dropdown. The runtimes available there come from this admin list.
+
+
+### Mail
+
+Two panels:
+
+- **Mail Server** — set the **Mail Server Hostname** (used for MX records on new domains and as the IMAP host for archival). Configure the Mail Server API (URL, Username, Password) that WHP uses to provision mailboxes. Toggle **Enable Mailserver API Debug Logging** when troubleshooting; it writes mailserver API requests/responses to the PHP error log.
+- **Outbound Email (SMTP)** — configure SMTP for outbound system alerts and customer AI Monitor notifications. Toggle **Enable Outbound Email** and provide the relay's credentials.
+
+### DNS
+
+Two panels:
+
+- **WHP Nameserver Configuration** — set the primary and secondary nameserver hostnames and IPs. These are baked into every customer's DNS zone, so changing them affects every domain you host.
+- **Network DNS Settings** — set the upstream resolvers the server uses (defaults to Cloudflare `1.1.1.1` and Google `8.8.8.8`).
+- **DNS Configuration Settings** — default TTL for new DNS records (60–86400 seconds).
+
+
+
+### Network & SSL
+
+Operational controls for HAProxy and system-service certs:
+
+- **HAProxy Configuration Management** — **Regenerate** (rebuild config for every active site), **Reload** (apply config without restart), **Health Check** (probe HAProxy).
+- **HAProxy API Key** — Bearer token used to authenticate against the HAProxy Manager API. After rotating, restart the HAProxy container.
+- **System Service SSL Certificates** — request a Let's Encrypt cert for system-level services like the WHP panel itself and FTP, by picking the service and the domain name.
+
+### Security
+
+API keys for **external integrations** like WHMCS. Not customer-facing.
+
+- **Create New API Key** — Key Name, Rate Limit (requests per hour), Permissions (User Management, Resource Management, SSO Access, System Statistics, AI Monitor Management), an optional IP whitelist, and Notes.
+- **Existing API Keys** — list of issued keys with their permissions and rate limit. Revoke by removing the row.
## Server-wide configuration files
-For settings that aren't exposed in the panel, you can SSH to the server and edit configuration directly:
+The host runs the WHP control panel and orchestrates customer containers. Most customer-affecting configuration lives **inside** containers, not on the host. A short map:
-- **Apache:** `/etc/httpd/conf.d/` for per-app drop-ins; per-site vhosts are generated from WHP and live in a directory the panel manages. **Don't** edit generated vhosts by hand — they'll be overwritten on the next config regeneration.
-- **PHP-FPM pools:** `/etc/php-fpm.d/` for runtime pool tweaks. Pool defaults are templated by WHP; edits to per-site pools are overwritten on regeneration.
-- **MySQL/MariaDB:** `/etc/my.cnf.d/`. The defaults are tuned for the server's resource profile; large-tweak changes are usually best left to a ticket so we can advise.
+- **Control-panel Apache:** `/etc/httpd/conf.d/` on the host configures the WHP panel's own Apache. Editing here changes how the panel serves; it doesn't change how customer sites serve.
+- **Control-panel PHP-FPM:** `/etc/php-fpm.d/` on the host configures the panel's PHP. Same scope.
+- **MySQL:** the MySQL instance runs as a container. Files under `/etc/my.cnf.d/` on the host are surfaced to customer database connections — they're effectively client-facing settings, not host settings.
+- **HAProxy:** runs as a container with its own volume. Reload via the **Network & SSL** tab; don't hand-edit files in the container.
-After editing, reload the relevant service from the WHP **Services** page (or via systemctl on the box).
+### Customer-side container customisation
-## Common admin tasks
+If a customer needs a non-standard runtime, library, or service inside their site's container — that's done by **building a custom Docker image** and adding it as a container type option in WHP, not by editing host-level config.
-**Drop a hot file cache.** From the **Services** page, click **Reload** on PHP-FPM. This is the right move after editing `php.ini` or an extension list.
+The pattern is documented in our cloud-container repos. See repo.anhonesthost.net/cloud-hosting-platform/ for the cloud-apache-container and cloud-node-container examples — they show the layout, build, and how to publish an image so it appears in the **Container Type** dropdown on the Sites page.
-**Free disk space on a full server.** Check `/var/log/` first — log rotation may be lagging. WHP rotates app logs into the per-site `logs/` directory; server-level logs in `/var/log` are yours to rotate via `logrotate` config in `/etc/logrotate.d/`.
-
-**See what's eating resources.** The admin **Resource usage** page shows aggregate CPU, RAM, disk I/O, and per-process drilldowns. For deeper inspection, SSH in and use `top`, `htop`, or `iotop`.
+
## Troubleshooting
-**Service won't restart.** The Services page surfaces the systemd error; if it's `failed (exit-code)`, check `journalctl -u ` on the box for the underlying message. The most common cause is a syntax error in a config file you just edited.
+**A service won't restart.** Check `journalctl -u ` (for systemd-managed services) or `docker logs ` (for containerized ones). The most common cause is a syntax error in a config file you just edited.
-**PHP module toggle has no effect.** PHP modules need a PHP-FPM **reload** to be picked up. The toggle should do this automatically; if it doesn't, click Reload manually.
+**Edits to a generated vhost keep disappearing.** That file is generated. Put your customisation in a per-app drop-in under `/etc/httpd/conf.d/`, or open a ticket about adding a stable include hook.
-**Edits to a generated vhost keep disappearing.** That file is generated. Put your customisation in a per-app drop-in under `/etc/httpd/conf.d/` instead, or open a ticket about adding a stable include hook.
+**Mailserver API debug log too noisy.** Toggle **Enable Mailserver API Debug Logging** off on the **Mail** tab once you've finished diagnosing.
## Related
diff --git a/src/content/docs/whp/admin/site-monitoring.mdx b/src/content/docs/whp/admin/site-monitoring.mdx
index 544db64..1c061ad 100644
--- a/src/content/docs/whp/admin/site-monitoring.mdx
+++ b/src/content/docs/whp/admin/site-monitoring.mdx
@@ -1,94 +1,112 @@
---
-title: Site Monitoring rules
-description: Configure Site Monitoring across every site on the server — global ignore lists, alert routing, and severity tuning.
+title: AI Monitor, Issues & Ignore Rules
+description: The three admin pages that drive the Site Monitoring add-on — AI Monitor dashboard, Issues, and Ignore Rules.
sidebar:
order: 4
- badge:
- text: Draft
- variant: caution
---
import { Aside } from '@astrojs/starlight/components';
import SuperAdmin from '~/content/partials/super-admin-callout.mdx';
-import Draft from '~/content/partials/draft-callout.mdx';
-import SignIn from '~/content/partials/signing-in.mdx';
+import AdminSignIn from '~/content/partials/admin-signin.mdx';
import Support from '~/content/partials/support-link.mdx';
-
+[Site Monitoring](/whp/add-ons/monitoring/) is the customer-facing alerting add-on. The admin side exposes three pages — together they let you tune what gets monitored, what surfaces as a customer-visible issue, and what gets suppressed.
-[Site Monitoring](/whp/add-ons/monitoring/) is the customer-facing alerting product. With super admin access, you can manage the rules that drive those alerts at the server level — including a server-wide ignore list, alert routing, and severity tuning.
+## Sign in as super admin
-## Sign in to WHP
+
-
+## AI Monitor (admin dashboard)
-## Where it lives
+Sidebar → **AI Monitor → Dashboard**. The operational heartbeat of the whole monitoring pipeline.
-Sidebar → **Site Monitoring** (admin view). The page shows two perspectives:
+
-- **Customer feed** — what your site owners see when they sign in.
-- **Admin tools** — the rule library, global ignore list, and alert configuration.
+Panels:
+
+- **AI Log Monitor Status** — overall on/off plus three sub-statuses:
+ - **Minute-cadence poll** — the cron that scans logs every minute.
+ - **Health API** — the internal API that exposes per-container health.
+ - **HAProxy stats** — the proxy stats feed used for error-rate tracking.
+- **Stat tiles** — Last Run, Errors Tracked, Remediations, API Calls Today (with a rate-limit denominator).
+- **Health Check Timeline (last 7d)** — every state transition (`cpu`, `swap`, `haproxy`, etc.) with its severity and an **AI Diagnosis** explanation.
+
+Use this page to confirm the pipeline is healthy and to drill into recent state changes. The AI Diagnosis text gives a plain-language summary of *why* a transition happened — useful for context before you act.
+
+## Issues
+
+Sidebar → **AI Monitor → Issues**. The customer-visible findings, server-wide.
+
+
+
+Four stat tiles at the top:
+
+- **Critical** — open critical-severity issues.
+- **Warning** — open warning-severity issues.
+- **Auto-resolved (review)** — issues the monitor closed on its own that may still need a human glance.
+- **Active suppressions** — issues currently muted by an Ignore Rule.
+
+Filter row: Scope (All / specific user), Status (Open / Closed / All), Severity, Source, Signature prefix.
+
+Bulk actions: **Mark Fixed**, **Ignore**, **Delete**.
+
+Each row has its own quick-actions: **Fix** (close it) and **Ignore** (create an ignore rule from this row's match criteria).
+
+## Ignore Rules
+
+Sidebar → **AI Monitor → Ignore Rules**. Match criteria that prevent matching findings from becoming customer-visible issues.
+
+
+
+Each rule has these fields:
+
+- **Scope** — `user` (just one customer) or `global` (every customer).
+- **Target** — the user or domain the rule applies to.
+- **Match** — a comma-separated set of `field=value` predicates (e.g. `cat=degraded & title~"Beaver Builder cache files missing"`). Match fields combine with **AND** semantics — every predicate must match.
+- **Reason** — a short note for future-you explaining why the rule exists.
+- **Hits / Last hit** — how often the rule has matched, and when.
+- **Enabled** — toggle without deleting.
+
+
## Common tasks
-### Add a global ignore rule
+### Mute a known-noisy finding for one customer
-When a signature is noisy for every site (for example, a known scanner you allow on your own infrastructure):
+1. Open **Issues**, find the row.
+2. Click the row's **Ignore** action — that pre-fills an Ignore Rule with the matching criteria scoped to that user.
+3. Add a **Reason** so future-you (or someone else on the team) understands why it exists.
+4. Save. Future matching findings will be suppressed; the **Active suppressions** tile will tick up.
-1. Open **Site Monitoring → Global Ignore Rules**.
-2. Click **Add Rule**.
-3. Define the match condition (rule_id, source IP/CIDR, URL pattern, or a combination).
-4. Add a short note so future-you remembers why this exists.
-5. Save. Matching events stop firing alerts immediately.
+### Investigate an "Auto-resolved (review)" issue
-
+These are issues where the underlying signal recovered before a human looked at them. Open the row to see the AI Diagnosis and the original detection. If the resolution looks legitimate, click **Mark Fixed**; if you're suspicious, leave it open and add a comment for context.
-### Tune severity for a rule
+### Tune brute-force detection
-If a rule is set to **critical** but you've decided it's really informational in your environment:
+Brute-force rules live in **Coraza Rules** (rule families CRS-913 / 921 / 942 / 949) rather than AI Monitor. AI Monitor surfaces the *effects* (error spikes) once a brute-force pattern fires, but the matching itself is in the WAF — see [Coraza WAF rules](/whp/admin/coraza-waf/).
-1. Find the rule in **Site Monitoring → Rules**.
-2. Open the rule and change its severity to one of: informational / warning / critical.
-3. Save. The severity change applies to new events from that rule.
+## Routing alerts
-Severity matters because **SMS notifications fire only on critical**. Downgrading from critical to warning silences SMS without silencing the rule.
-
-### Route alerts somewhere other than the default
-
-Default routing: alerts go to the contact email on the account. You can:
-
-- **Add additional email recipients** — useful for a shared ops alias.
-- **Enable SMS for critical alerts** — wire your phone number on the **Alert Routing** page.
-- **Forward to a webhook** — for integration with Slack, PagerDuty, or your own incident pipeline.
-
-## Brute-force detection
-
-Brute-force detection is a separate rule family and has its own per-site sensitivity. From the admin view, you can adjust:
-
-- The window in which repeated failures count.
-- The threshold at which the rule fires.
-- Whether the rule auto-blocks the source IP (recommended) or only alerts.
-
-## Things to know
-
-- **Ignore lists don't stop logging.** They only suppress alerts. The events still appear in the audit feed so you can see what's actually happening.
-- **Rules apply on the next log scan**, typically within a minute.
-- **A muted rule for one customer doesn't affect others.** Per-site ignore is scoped tightly.
+Customer email alerts are sent via the SMTP relay configured on **Server Settings → Mail → Outbound Email (SMTP)**. Toggle **Enable Outbound Email** off there if you need to silence outbound notifications for a maintenance window.
## Troubleshooting
-**No alerts arriving for a known event.** Check the **Global Ignore Rules** list and any per-site ignores. Also confirm the rule's severity isn't set to informational (no email or SMS by default).
+**Pipeline shows "stale" — Last Run more than a few minutes ago.** Check the cron's container on **Server Settings → Services → Docker Container Management**. The monitor poll is `whp-monitor-poll`.
-**SMS not firing on critical.** Confirm SMS is enabled in **Alert Routing** and the phone number is verified.
+**Issues appearing for a known noisy site.** Add an Ignore Rule scoped to that user.
+
+**No customer alerts arriving.** Confirm Outbound Email is enabled and the SMTP relay is reachable from the server.
## Related
-- [Site Monitoring add-on](/whp/add-ons/monitoring/) — what the customer sees.
-- [Coraza WAF rules](/whp/admin/coraza-waf/) — request-level firewall, complements monitoring.
+- [Site Monitoring add-on](/whp/add-ons/monitoring/) — the customer-facing side.
+- [Coraza WAF rules](/whp/admin/coraza-waf/) — request-level firewall.
+- [Server settings & services](/whp/admin/server-settings/) — SMTP relay config for outbound alerts.
## Still stuck?
diff --git a/src/content/docs/whp/admin/user-management.mdx b/src/content/docs/whp/admin/user-management.mdx
index acef299..7b1d4b5 100644
--- a/src/content/docs/whp/admin/user-management.mdx
+++ b/src/content/docs/whp/admin/user-management.mdx
@@ -1,95 +1,144 @@
---
title: Users & delegated access
-description: Create sub-users, delegate panel access, and manage SFTP/SSH users at the server level.
+description: Create WHP users, set account types, change passwords, plus delegated user access and account suspensions.
sidebar:
order: 5
- badge:
- text: Draft
- variant: caution
---
import { Aside } from '@astrojs/starlight/components';
import SuperAdmin from '~/content/partials/super-admin-callout.mdx';
-import Draft from '~/content/partials/draft-callout.mdx';
-import SignIn from '~/content/partials/signing-in.mdx';
+import AdminSignIn from '~/content/partials/admin-signin.mdx';
import Support from '~/content/partials/support-link.mdx';
-
+Four admin pages collectively control who can sign in and what they can do on the server:
-WHP super admin lets you give other people scoped access to the server — your dev team, a contractor, or a junior admin — without sharing your own credentials.
+- **User Management** — create / change-password / delete WHP users.
+- **User Resources** — set CPU / RAM / disk allowances per user.
+- **Delegated Users** — list of contractor / sub-account grants on customer sites.
+- **Account Suspensions** — suspended accounts.
-## Three kinds of access
+## Sign in as super admin
-| Type | What they can do | Where they sign in |
-| ------------------- | -------------------------------------------------------------------------------- | --------------------------------------- |
-| **WHP sub-user** | Sign in to WHP with their own credentials. You control which sections they see. | Same `:8443` URL as you. |
-| **Delegated panel access** | A read-only or scoped-write view onto a specific site, for a contractor. | Same WHP, but scoped to that one site. |
-| **SFTP / SSH user** | File access (and optionally SSH) without WHP at all. | SFTP client / SSH terminal. |
+
-## Sign in to WHP
+## User Management
-
+Sidebar → **User Management**.
-## Create a WHP sub-user
+
+
+### Create New User
+
+Every user created here is a **customer account**, not a super admin. (Super admin is the `root` user on the server, and there's no UI to add another.)
+
+Fields:
+
+- **Username** — UNIX-safe username; also becomes the SFTP user and home-directory name (`/docker/users/`).
+- **Password** — strong password. The user can change it later from the panel.
+- **Account Type** — pick the scope of features this customer should see:
+ - **Full Hosting** — sites, databases, domains, DNS, email. The default for normal customers.
+ - **Domain/DNS Only** — domains and DNS records only (no sites, databases, or email).
+ - **Mail/DNS Only** — email plus domains/DNS (no sites or databases).
+
+Click **Create User** to provision the account. The user's home directory and SFTP credentials are set up immediately.
-1. Open **Users → WHP Users → Add User** in the admin sidebar.
-2. Set a username, a strong password, and an email (used for password reset and 2FA).
-3. Choose a **role**: pick from the predefined roles (Admin, Site Manager, Read-only, etc.) or build a custom role with specific pages enabled.
-4. (Optional) Enable **Require 2FA** so they have to set up an authenticator app on first login.
-5. Save. Share the credentials with them out-of-band; don't email passwords.
+### Change User Password
-## Delegated access for a single site
+Pick the user from the dropdown, enter a new password, click **Change Password**. The user is forced to sign in again on next visit; any in-flight panel sessions are still live until you also revoke them via **Active Sessions**.
-Use the customer-facing **Delegated Users** page (sidebar → **Delegated Users**, available on every account) when a contractor only needs to work on one site:
+### User Accounts table
-1. Open **Delegated Users → Add**.
-2. Pick the site they should have access to.
-3. Set their permission scope: view-only, manage-files, manage-DNS, etc.
-4. Send them the panel URL. They sign in with their own credentials and see only that site.
+Columns: **Username**, **UID** (UNIX uid), **Account Type**, **Home Directory**, **Actions** (Account-Type dropdown + Delete).
-This is the right path for, say, a freelance designer who needs to upload assets but shouldn't see your other sites or your DNS.
+To change a user's account type, change the dropdown in the row and the change applies immediately. The **System** badge on a row marks an internal/system user (such as `root`, `whp`, `daemon`, `www-data`, `nobody`, the various `systemd-*` users, etc.).
-## SFTP / SSH users
-
-Pure file access without WHP. Created from the admin **Users → SFTP/SSH** page:
-
-1. Open **Users → SFTP/SSH → Add**.
-2. Set the username, password (or paste their public SSH key), and which directories they have access to.
-3. Pick whether to grant interactive SSH or restrict to SFTP only.
-4. Save. They can now connect with their preferred SFTP/SSH client.
+**System users are protected.** The panel refuses to delete any user on the protected list — `delete_user` in `web-files/libs/usermgmt.php` checks `is_protected_user($username)` first and returns *"Cannot delete protected system user"* without touching the OS user. Password changes are blocked the same way, with one exception: `root`'s password can be changed (the rest of the protected list cannot).
-## Managing existing users
+## User Resources
-The user list shows last sign-in, role, and 2FA status. Common actions from each user's row:
+Sidebar → **User Resources**. Configure CPU and memory allowances per user.
-- **Disable** — keeps the user but blocks sign-in.
-- **Delete** — removes the user.
-- **Force password reset** — invalidates their current password; they receive an email link.
-- **Revoke sessions** — kicks them out of any active panel sessions immediately.
+
-When someone leaves your team, **revoke sessions first** (so they're out *now*), then disable or delete the user.
+Each row shows current allocation vs. usage:
+
+- **Max CPU** / **CPU Used** — in 0.25-core increments.
+- **Max Mem** / **Mem Used** — in 256 MB increments.
+- **Disk** / **Used** — total disk allocation and current consumption (with %).
+- **Email** — mailbox slot count.
+- **Mail MB** — total mail storage cap.
+- **Arch.** — archival email slots used / total.
+- **Cont.** — currently-running container count.
+
+Use the **Actions** column to edit a user's caps. Changes apply on the next container restart for that user's sites.
+
+
+
+## Delegated Users
+
+Sidebar → **Delegated Users**. Customers can use the Delegated Users page on their own account to grant a contractor scoped access to one of their sites. The admin view shows every active delegation across the server.
+
+From the admin view you can:
+
+- **Audit** — see who has cross-account access at a glance.
+- **Edit** — modify scope, permissions, or expiry on any delegation for an independent customer. Useful when a customer asks support to fix a grant they set up incorrectly.
+- **Revoke** — remove a delegation outright.
+
+If a customer reports a delegation issue, this page is where you confirm the grant exists, inspect its scope, and adjust it on their behalf.
+
+## Account Suspensions
+
+Sidebar → **Account Suspensions**. The list of suspended customer accounts.
+
+A suspension takes a customer's sites offline without deleting any data — the customer can be reinstated by removing the suspension. Useful for non-payment, terms-of-service issues, or maintenance hold.
+
+The page lists who's suspended, when, by whom, and why. Reinstate from the action button on each row.
+
+### How the suspension page is served
+
+The "site suspended" page is served by **HAProxy** itself, not by a separate backend. When you suspend an account, WHP rewrites the HAProxy config to point that account's frontends at a 503 errorfile (`/usr/local/etc/haproxy/errors/503.http`) and reloads HAProxy.
+
+**If a suspended site is still serving the real content** (or is throwing a network error instead of the suspended page), it almost always means HAProxy didn't pick up the config reload. Check, in order:
+
+1. **HAProxy container is running.** **Server Settings → Services → Docker Container Management** → confirm `Haproxy manager` shows **Running**.
+2. **HAProxy reload succeeded.** Either re-trigger from **Server Settings → Network & SSL → Reload Configuration**, or check the HAProxy container logs (`docker logs haproxy-manager`) for a reload error — usually a syntax error in the generated config from the suspension action.
+3. **Errorfile is in place.** The 503 page lives at `/usr/local/etc/haproxy/errors/503.http` inside the container.
+
+## Active Sessions
+
+Sidebar → **Active Sessions**. Every active panel session across the server, with last activity time, IP, and a **Terminate** button. Use this when offboarding someone — kick them out of any active sessions *first*, then change their password or delete the user.
+
+## When someone leaves your team
+
+1. **Revoke active sessions** for that user via **Active Sessions**.
+2. **Change their password** in **User Management** (locks them out even if they save their cookies).
+3. **Downgrade or delete** the user in **User Management**.
+4. Audit **Delegated Users** for any cross-account delegations that should also be revoked.
## Troubleshooting
-**Sub-user can sign in but the page they expect is missing.** Their role doesn't include that section. Edit the role and tick the right page.
+**"Cannot delete protected system user".** Expected — system users (root, daemon, www-data, mail, the `systemd-*` users, etc.) are blocked at the panel level to prevent breaking the host. If you really need to remove a user, confirm it's a customer account first.
-**SFTP user can connect but uploads land in the wrong directory.** Check their **Home directory** in the SFTP/SSH user page — it determines what they see as `/`.
+**Created user can't sign in.** Confirm the password meets the strength rules. If the user is signing in for the first time, they may be hitting the password-change-on-first-login flow.
-**A delegated user can't see DNS records.** Delegated access defaults to file-only. Edit their permissions to include DNS Management.
+**Suspended customer's sites are still serving real traffic.** The suspension page is served by HAProxy — see "How the suspension page is served" above. Most often it's an HAProxy reload that didn't happen; check the container logs.
## Related
-- [Server settings & services](/whp/admin/server-settings/)
+- [Server settings & services](/whp/admin/server-settings/) — including the suspension backend service.
+- [AI Monitor, Issues & Ignore Rules](/whp/admin/site-monitoring/)
## Still stuck?
diff --git a/src/content/docs/whp/how-to/backups.mdx b/src/content/docs/whp/how-to/backups.mdx
index 106cd13..de9cec6 100644
--- a/src/content/docs/whp/how-to/backups.mdx
+++ b/src/content/docs/whp/how-to/backups.mdx
@@ -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';
-WHP keeps automatic backups of your sites and databases. The **Backup Management** page lets you trigger an on-demand backup, add a scheduled backup, manage where backups are sent, and review history.
+The **Backup Management** page lets you trigger an on-demand backup, add a scheduled backup, manage where backups are sent, and review history.
## What's backed up
@@ -18,6 +18,10 @@ WHP keeps automatic backups of your sites and databases. The **Backup Management
Default retention on built-in backup targets is **5 days, up to 10 backups**.
+
+
## Sign in to WHP
@@ -83,6 +87,14 @@ Don't do a full restore unless you genuinely need to — it rewrites your live s
**"No targets available" when starting a backup.** Your account has no backup targets attached. Open a ticket; we'll get one wired up.
+## What's *not* in customer backups
+
+Customer backups cover your sites and databases — they don't cover the underlying server, the OS, or system-level config. Full-server backups are a separate concern:
+
+- On our managed plans, **AnHonestHost handles full-server backups** for the host.
+- On a Virtual Dedicated Server (VDS) we provide, **full-server snapshots are included** at the platform level.
+- If WHP is running somewhere else (your own infrastructure), full-server backups are the server operator's responsibility — WHP itself doesn't provide a host-level backup tool.
+
## Related
- [Archival email add-on](/whp/add-ons/archival-email/)
diff --git a/src/content/partials/admin-signin.mdx b/src/content/partials/admin-signin.mdx
new file mode 100644
index 0000000..5d7868f
--- /dev/null
+++ b/src/content/partials/admin-signin.mdx
@@ -0,0 +1,3 @@
+Super admin access is granted to the `root` user only. Sign in **directly** at `https://:8443` with the root credentials.
+
+The WHMCS client portal route doesn't apply for super admin — it signs you in as the linked customer, not as root.
diff --git a/tools/screenshots/capture-admin.ts b/tools/screenshots/capture-admin.ts
new file mode 100644
index 0000000..c1ee869
--- /dev/null
+++ b/tools/screenshots/capture-admin.ts
@@ -0,0 +1,229 @@
+/**
+ * v2 admin capture — stricter redaction and deeper navigation.
+ *
+ * - Masks customer domains (anything not in the brand allowlist).
+ * - Masks customer-shaped usernames (anything not in the system allowlist).
+ * - Masks input value attributes (the v1 only walked text nodes).
+ * - Captures Settings sub-tabs (System, Services, Mail, DNS, Network & SSL,
+ * Security) since those are where LiteLLM URL / model / key likely live.
+ *
+ * Read-only. Never clicks save/apply/restart/delete.
+ */
+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_ADMIN_USER');
+const PASS = need('WHP_ADMIN_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(() => {
+ // ---- Mask all secret-shaped input values ----
+ const secretInputSel = [
+ 'input[type="password"]',
+ 'input[name*="key" i]',
+ 'input[name*="token" i]',
+ 'input[name*="secret" i]',
+ 'input[name*="api" i]',
+ ];
+ for (const sel of secretInputSel) {
+ document.querySelectorAll(sel).forEach((el) => {
+ if (el.value) el.value = '████████████████';
+ });
+ }
+
+ // ---- Brand allowlist ----
+ const BRAND_SUFFIXES = [
+ 'anhonesthost.com', 'anhonesthost.net', 'anhonesthost.io',
+ 'anhh.co',
+ 'cloud-hosting.io',
+ 'example.com', 'example.org', 'example.net',
+ ];
+
+ // ---- System users to keep visible (others get masked) ----
+ const SYSTEM_USERS_ARR = ['root', 'admin', 'whp', 'haproxy', 'apache', 'nginx', 'newuser'];
+
+ // ---- Text-node swaps ----
+ const swaps: [RegExp, string][] = [
+ // Server / mail / nameserver hostnames in our infra
+ [/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/gi, ''],
+ [/WHP\d+(-[A-Z0-9]+)?\b/g, ''],
+ // IPs
+ [/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, ''],
+ // Home dirs
+ [/\/docker\/users\/[a-z0-9-]+/g, '/docker/users/'],
+ // Common secret shapes
+ [/sk-[A-Za-z0-9_-]{20,}/g, ''],
+ [/sk_(test|live)_[A-Za-z0-9]{20,}/g, ''],
+ [/Bearer\s+[A-Za-z0-9._-]{20,}/g, 'Bearer '],
+ [/eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g, ''],
+ // AI provider URLs
+ [/https?:\/\/[^\s"'<>]*litellm[^\s"'<>]*/gi, ''],
+ [/https?:\/\/[^\s"'<>]*\.anhonesthost\.(net|com|io)[^\s"'<>]*/gi, ''],
+ // Model family identifiers
+ [/(claude|gpt|llama|mistral|gemini)[a-z0-9._-]*-\d[a-z0-9.\-]*/gi, ''],
+ // Bichon / Coraza / haproxy internal endpoints
+ [/https?:\/\/[^\s"'<>]*\b(bichon|coraza-spoa|haproxy-manager)[^\s"'<>]*/gi, ''],
+ // root → admin (we don't expose which UNIX user has super admin)
+ [/Welcome, root\b/g, 'Welcome, admin'],
+ [/(User:\s*)root\b/g, '$1admin'],
+ [/(Home Directory:\s*)\/root\b/g, '$1/'],
+ // Standalone 'root' (whole-word, not preceded by / or . — so paths like
+ // /root/foo and references like .root stay untouched).
+ [/(^|[^/.\w])root\b/g, '$1admin'],
+ ];
+
+ // ---- Walk text nodes ----
+ 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 ?? '';
+
+ // Mask non-brand domain names (basic pattern: word.tld)
+ v = v.replace(/\b([a-z0-9][a-z0-9-]{0,62}\.)+[a-z]{2,24}\b/gi, function (m) {
+ const h = m.toLowerCase();
+ let brand = false;
+ for (const s of BRAND_SUFFIXES) { if (h === s || h.endsWith('.' + s)) { brand = true; break; } }
+ return brand ? m : '';
+ });
+
+ // Apply other swaps
+ for (const [re, replacement] of swaps) v = v.replace(re, replacement);
+
+ if (v !== node.nodeValue) node.nodeValue = v;
+ }
+
+ // ---- Mask sensitive content in input values ----
+ document.querySelectorAll('input[type="text"], input[type="url"], input[type="email"], input:not([type])').forEach((el) => {
+ if (!el.value) return;
+ const v = el.value;
+ let nv = v.replace(/\b([a-z0-9][a-z0-9-]{0,62}\.)+[a-z]{2,24}\b/gi, function (m) {
+ const h = m.toLowerCase();
+ let brand = false;
+ for (const s of BRAND_SUFFIXES) { if (h === s || h.endsWith('.' + s)) { brand = true; break; } }
+ return brand ? m : '';
+ });
+ // Server / mail / NS hostnames
+ nv = nv.replace(/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '.cloud-hosting.io');
+ nv = nv.replace(/mail\d+\.cloud-hosting\.io/gi, '.cloud-hosting.io');
+ nv = nv.replace(/ns[12]\.whp\d+\.cloud-hosting\.io/gi, 'ns..cloud-hosting.io');
+ // IPv4 — skip the well-known public resolvers / RFC1918 examples
+ nv = nv.replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, function (ip) {
+ const pub = new Set(['1.1.1.1', '1.0.0.1', '8.8.8.8', '8.8.4.4', '9.9.9.9', '149.112.112.112', '208.67.222.222', '208.67.220.220']);
+ if (pub.has(ip)) return ip;
+ if (/^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(ip)) return ip; // private nets, fine
+ return '';
+ });
+ if (nv !== v) el.value = nv;
+ });
+
+ // ---- Mask customer usernames in table cells ----
+ // Heuristic: find table cells under a header containing 'user', 'username', or 'target' (case-insensitive)
+ document.querySelectorAll('table').forEach((tbl) => {
+ const headers = Array.from(tbl.querySelectorAll('thead th, thead td')).map(th => (th.textContent || '').trim().toLowerCase());
+ const userCols: number[] = [];
+ for (let i = 0; i < headers.length; i++) {
+ if (/^(user(name)?|target|owner|account)$/.test(headers[i])) userCols.push(i);
+ }
+ if (userCols.length === 0) return;
+ const rows = tbl.querySelectorAll('tbody tr');
+ for (let r = 0; r < rows.length; r++) {
+ const cells = rows[r].querySelectorAll('td');
+ for (const idx of userCols) {
+ const cell = cells[idx];
+ if (!cell) continue;
+ const txt = (cell.textContent || '').trim();
+ if (!txt) continue;
+ let isSystem = false;
+ for (const u of SYSTEM_USERS_ARR) { if (u === txt) { isSystem = true; break; } }
+ if (!isSystem) cell.textContent = '';
+ }
+ }
+ });
+ });
+}
+
+async function shot(page: Page, id: string) {
+ await page.waitForLoadState('networkidle');
+ await page.waitForTimeout(500);
+ await redact(page);
+ const path = resolve(OUT_DIR, `${id}.png`);
+ await page.screenshot({ path, fullPage: false });
+ console.log(`captured ${id}`);
+}
+
+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);
+
+ // Server Settings — click each tab
+ await page.goto(`${BASE}/index.php?page=server-settings`);
+ await page.waitForLoadState('networkidle');
+ await shot(page, 'admin-srvset-system');
+ for (const tab of ['services', 'mail', 'dns', 'network', 'security']) {
+ const trigger = page.locator(`[data-bs-target="#tab-${tab}"]`).first();
+ if (await trigger.count() === 0) { console.log('tab trigger not found:', tab); continue; }
+ await trigger.click().catch(()=>{});
+ await page.waitForTimeout(1200);
+ await shot(page, `admin-srvset-${tab}`);
+ }
+
+ // Re-shoot the previously PII-heavy pages with v2 redaction
+ const rerun = [
+ { id: 'admin-coraza', path: '/index.php?page=coraza-rules' },
+ { id: 'admin-monitor-admin', path: '/index.php?page=ai-monitor' },
+ { id: 'admin-ignore-rules', path: '/index.php?page=ai-monitor-ignore-rules' },
+ { id: 'admin-user-mgmt', path: '/index.php?page=user-management' },
+ { id: 'admin-user-resources', path: '/index.php?page=user-resources' },
+ { id: 'admin-issues', path: '/index.php?page=issues' },
+ { id: 'admin-suspensions', path: '/index.php?page=account-suspensions' },
+ { id: 'admin-disk-usage', path: '/index.php?page=disk-usage' },
+ { id: 'admin-docker', path: '/index.php?page=docker-management' },
+ { id: 'admin-valkey', path: '/index.php?page=valkey-admin' },
+ { id: 'admin-updates', path: '/index.php?page=update-management' },
+ { id: 'admin-container-boot', path: '/index.php?page=container-boot' },
+ { id: 'admin-site-audit', path: '/index.php?page=site-audit' },
+ { id: 'admin-delegated', path: '/index.php?page=delegated-users' },
+ ];
+ for (const r of rerun) {
+ await page.goto(`${BASE}${r.path}`);
+ await page.waitForLoadState('networkidle');
+ await shot(page, r.id);
+ }
+ } finally {
+ await browser.close();
+ }
+}
+
+main().catch(e => { console.error(e); process.exit(1); });