Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fef98357c7 | ||
|
|
5d829c44ba | ||
|
|
2cd10bc56d | ||
|
|
ccfe8bb649 | ||
|
|
c602b8f8f3 | ||
|
|
53bc37fd0d | ||
|
|
af94b72777 | ||
|
|
16e14019b7 | ||
|
|
e3b113cc2f | ||
|
|
748fcfeb6f |
@@ -20,9 +20,10 @@ export default defineConfig({
|
||||
starlight({
|
||||
title: 'AnHonestHost KB',
|
||||
description: 'Customer documentation for WHP and other AnHonestHost services.',
|
||||
logo: {
|
||||
src: './src/assets/logo.svg',
|
||||
replacesTitle: false,
|
||||
components: {
|
||||
// Inline-SVG brand mark + "Knowledge Base" label.
|
||||
// Inlining lets the SVG's currentColor follow the active theme.
|
||||
SiteTitle: './src/components/SiteTitle.astro',
|
||||
},
|
||||
customCss: [
|
||||
'@fontsource-variable/inter',
|
||||
@@ -52,6 +53,11 @@ export default defineConfig({
|
||||
label: 'Add-ons',
|
||||
items: [{ autogenerate: { directory: 'whp/add-ons' } }],
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
badge: { text: 'Draft', variant: 'caution' },
|
||||
items: [{ autogenerate: { directory: 'whp/admin' } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Future products only appear once they have content.
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.9",
|
||||
"@types/node": "^25.8.0",
|
||||
"playwright": "^1.60.0",
|
||||
"tsx": "^4.22.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
},
|
||||
@@ -2057,12 +2060,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz",
|
||||
"integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
|
||||
"version": "25.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
|
||||
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sax": {
|
||||
@@ -5384,6 +5387,53 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
|
||||
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.60.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
|
||||
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||
@@ -6061,6 +6111,21 @@
|
||||
"npm": ">=10.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sitemap/node_modules/@types/node": {
|
||||
"version": "24.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz",
|
||||
"integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sitemap/node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/smol-toml": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz",
|
||||
@@ -6259,6 +6324,509 @@
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.1.tgz",
|
||||
"integrity": "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
||||
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/esbuild": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.0",
|
||||
"@esbuild/android-arm": "0.28.0",
|
||||
"@esbuild/android-arm64": "0.28.0",
|
||||
"@esbuild/android-x64": "0.28.0",
|
||||
"@esbuild/darwin-arm64": "0.28.0",
|
||||
"@esbuild/darwin-x64": "0.28.0",
|
||||
"@esbuild/freebsd-arm64": "0.28.0",
|
||||
"@esbuild/freebsd-x64": "0.28.0",
|
||||
"@esbuild/linux-arm": "0.28.0",
|
||||
"@esbuild/linux-arm64": "0.28.0",
|
||||
"@esbuild/linux-ia32": "0.28.0",
|
||||
"@esbuild/linux-loong64": "0.28.0",
|
||||
"@esbuild/linux-mips64el": "0.28.0",
|
||||
"@esbuild/linux-ppc64": "0.28.0",
|
||||
"@esbuild/linux-riscv64": "0.28.0",
|
||||
"@esbuild/linux-s390x": "0.28.0",
|
||||
"@esbuild/linux-x64": "0.28.0",
|
||||
"@esbuild/netbsd-arm64": "0.28.0",
|
||||
"@esbuild/netbsd-x64": "0.28.0",
|
||||
"@esbuild/openbsd-arm64": "0.28.0",
|
||||
"@esbuild/openbsd-x64": "0.28.0",
|
||||
"@esbuild/openharmony-arm64": "0.28.0",
|
||||
"@esbuild/sunos-x64": "0.28.0",
|
||||
"@esbuild/win32-arm64": "0.28.0",
|
||||
"@esbuild/win32-ia32": "0.28.0",
|
||||
"@esbuild/win32-x64": "0.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typesafe-path": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/typesafe-path/-/typesafe-path-0.2.2.tgz",
|
||||
@@ -6309,9 +6877,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unified": {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"check": "astro check",
|
||||
"screenshots": "tsx tools/screenshots/run.ts",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -20,6 +21,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.9",
|
||||
"@types/node": "^25.8.0",
|
||||
"playwright": "^1.60.0",
|
||||
"tsx": "^4.22.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 32" width="120" height="32">
|
||||
<text x="0" y="22" font-family="Inter,system-ui,sans-serif" font-size="20" font-weight="700" fill="#00d4aa">AHH</text>
|
||||
<text x="50" y="22" font-family="Inter,system-ui,sans-serif" font-size="20" font-weight="400" fill="currentColor">KB</text>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 32" fill="none" height="32" role="img" aria-label="AnHonestHost Knowledge Base">
|
||||
<path d="M12 4L3 16L12 28" stroke="#00d4aa" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24 4L33 16L24 28" stroke="#00d4aa" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="12" y1="16" x2="24" y2="16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.35"/>
|
||||
<text x="44" y="21" font-family="Inter, system-ui, -apple-system, sans-serif" font-size="18" font-weight="800" fill="currentColor" letter-spacing="-0.5">AnHonest<tspan fill="#00d4aa">Host</tspan></text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 340 B After Width: | Height: | Size: 700 B |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 534 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 236 KiB |
@@ -0,0 +1,84 @@
|
||||
---
|
||||
const { siteTitleHref } = Astro.locals.starlightRoute;
|
||||
---
|
||||
|
||||
<div class="site-title-row sl-flex">
|
||||
<a href={siteTitleHref} class="site-title sl-flex" translate="no" aria-label="AnHonestHost Knowledge Base">
|
||||
<svg
|
||||
class="brand-logo"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 240 32"
|
||||
fill="none"
|
||||
height="28"
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 4L3 16L12 28" stroke="#00d4aa" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24 4L33 16L24 28" stroke="#00d4aa" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="12" y1="16" x2="24" y2="16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.35"/>
|
||||
<text x="44" y="21" font-family="Inter, system-ui, -apple-system, sans-serif" font-size="18" font-weight="800" fill="currentColor" letter-spacing="-0.5">AnHonest<tspan fill="#00d4aa">Host</tspan></text>
|
||||
</svg>
|
||||
<span class="kb-label">Knowledge Base</span>
|
||||
</a>
|
||||
|
||||
<nav class="ext-nav" aria-label="AnHonestHost links">
|
||||
<a href="https://anhonesthost.com/" class="ext-link">Get a Plan</a>
|
||||
<a href="https://secure.anhonesthost.com/clientarea.php" class="ext-link">Client Portal</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.site-title-row {
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.site-title {
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
color: var(--sl-color-text);
|
||||
}
|
||||
.brand-logo {
|
||||
height: calc(var(--sl-nav-height) - 2 * var(--sl-nav-pad-y));
|
||||
width: auto;
|
||||
}
|
||||
.kb-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--anhh-text-secondary, #94a3b8);
|
||||
padding-left: 0.75rem;
|
||||
border-left: 1px solid var(--anhh-border-color, #334155);
|
||||
line-height: 1;
|
||||
}
|
||||
.ext-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
.ext-link {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--anhh-text-secondary, #94a3b8);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: color 120ms ease;
|
||||
}
|
||||
.ext-link:hover,
|
||||
.ext-link:focus {
|
||||
color: var(--anhh-accent, #00d4aa);
|
||||
outline: none;
|
||||
}
|
||||
/* Hide kb-label and external nav on narrow viewports — search + hamburger
|
||||
get tight on mobile. */
|
||||
@media (max-width: 60em) {
|
||||
.kb-label {
|
||||
display: none;
|
||||
}
|
||||
.ext-nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Archival email
|
||||
description: Long-term, searchable archive of your mailbox content, separate from the live mailbox.
|
||||
sidebar:
|
||||
order: 3
|
||||
---
|
||||
|
||||
import PaidAddon from '~/content/partials/paid-addon-callout.mdx';
|
||||
import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
<PaidAddon />
|
||||
|
||||
## What it does
|
||||
|
||||
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.
|
||||
- Compliance or policy requires you keep email for a fixed window.
|
||||
- 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.
|
||||
|
||||
## How it's different from backups
|
||||
|
||||
| | Backups | Archival email |
|
||||
|---|---|---|
|
||||
| **Type** | Point-in-time snapshots | Continuous mailbox copy |
|
||||
| **Granularity** | Restore the whole mailbox to a moment in time | Search and retrieve individual messages |
|
||||
| **Best for** | Recovering from a known event ("the mailbox was hacked yesterday") | Long-term retention, occasional message lookup |
|
||||
|
||||
You can use both — they cover different problems.
|
||||
|
||||
## 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).
|
||||
- **14-day quick-restore window.** Accidentally deleted mail is recoverable without staff help.
|
||||
- **Configurable longer retention.** Set retention to match your policy.
|
||||
- **Independent password reset on the archive.** Grant audit access without disturbing the live mailbox.
|
||||
|
||||
## How to enable
|
||||
|
||||
From the [client portal](https://secure.anhonesthost.com/clientarea.php), open **Add-ons** under your hosting service and pick Archival email. Billing is per archived mailbox.
|
||||
|
||||
## How to use it
|
||||
|
||||
The archive is managed alongside your regular mailboxes on the **Email** page in WHP — the **Email Accounts** section shows archive status per mailbox, and the per-mailbox menu lets you search and restore archived mail.
|
||||
|
||||
## Related
|
||||
|
||||
- [Create an email account](/whp/how-to/create-an-email-account/)
|
||||
- [Backups](/whp/how-to/backups/)
|
||||
- [Email upgrades](/whp/add-ons/email-upgrades/)
|
||||
|
||||
## Still stuck?
|
||||
|
||||
<Support />
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Email upgrades
|
||||
description: Add mailboxes or bump per-mailbox storage on your plan.
|
||||
sidebar:
|
||||
order: 5
|
||||
---
|
||||
|
||||
import PaidAddon from '~/content/partials/paid-addon-callout.mdx';
|
||||
import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
<PaidAddon />
|
||||
|
||||
## What it does
|
||||
|
||||
Add **more mailboxes** than your plan includes, or **bump the per-mailbox storage cap** so individual mailboxes can hold more mail.
|
||||
|
||||
## What you can upgrade
|
||||
|
||||
- **Mailbox count** — extra accounts beyond your plan's included number.
|
||||
- **Per-mailbox size** — raise the storage cap on individual mailboxes (extra GB).
|
||||
- **Shared email pool** — if your plan uses a shared storage pool across all mailboxes, you can increase the pool size.
|
||||
|
||||
## When you might need this
|
||||
|
||||
- **Hiring more staff** and you've used up your included mailboxes.
|
||||
- **A mailbox is approaching its storage cap.** First consider whether [archival email](/whp/add-ons/archival-email/) is the better fit — if the issue is *old* mail you rarely need, archiving is cheaper and keeps the live mailbox responsive.
|
||||
- **A domain that sends newsletters** and needs many sending aliases.
|
||||
|
||||
## How to enable
|
||||
|
||||
From the [client portal](https://secure.anhonesthost.com/clientarea.php), open your hosting service → **Upgrade/Downgrade** → pick the email option. WHP applies it immediately.
|
||||
|
||||
## Related
|
||||
|
||||
- [Create an email account](/whp/how-to/create-an-email-account/)
|
||||
- [Archival email](/whp/add-ons/archival-email/)
|
||||
|
||||
## Still stuck?
|
||||
|
||||
<Support />
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: Site Monitoring
|
||||
description: Proactive alerts for site errors, brute-force attempts, and exploit signatures.
|
||||
sidebar:
|
||||
order: 2
|
||||
---
|
||||
|
||||
import PaidAddon from '~/content/partials/paid-addon-callout.mdx';
|
||||
import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
<PaidAddon />
|
||||
|
||||
## What it does
|
||||
|
||||
Site Monitoring watches your site's logs, error rates, and access patterns in the background. If something looks unusual — a sustained spike in 500 errors, a brute-force pattern against a login page, a request matching a known exploit signature — you get an alert.
|
||||
|
||||
By default, alerts go to the contact email on your account. SMS is available for critical-severity events if you opt in.
|
||||
|
||||
## What's included
|
||||
|
||||
- **Real-time log scanning** across PHP, Apache, and access logs.
|
||||
- **Brute-force detection** on auth endpoints (WordPress wp-login.php, generic /login, mail auth, SFTP).
|
||||
- **Exploit-signature alerts** with severity grading (informational, warning, critical).
|
||||
- **Per-rule ignore lists** so you can mute things you've already triaged or know are harmless for your site.
|
||||
- **SMS notifications** for critical-severity alerts (configurable; off by default).
|
||||
|
||||
## How to enable
|
||||
|
||||
Site Monitoring is enabled per site. From the [client portal](https://secure.anhonesthost.com/clientarea.php), go to your hosting service → **Upgrade/Downgrade** and pick Site Monitoring.
|
||||
|
||||
## Where it lives in WHP
|
||||
|
||||
Once enabled, find it in the sidebar: **Site Monitoring**.
|
||||
|
||||

|
||||
|
||||
When the add-on isn't enabled, the page reads: *"AI monitoring is not enabled for any of your sites. Contact your hosting provider to enable this feature."*
|
||||
|
||||
Once enabled, the page shows a feed of events with severity badges and counts. Click an event for the full log context.
|
||||
|
||||
## Tuning false positives
|
||||
|
||||
Some signatures are noisier than others — for example, a security scanner you run yourself will trip every exploit-signature rule. Open the rule from the event detail and click **Ignore** to mute it for your account; the rule stays active for everyone else.
|
||||
|
||||
If you find yourself muting a rule frequently, [open a ticket](https://secure.anhonesthost.com/submitticket.php) — we may be able to tune the underlying rule for everyone instead of you suppressing each instance.
|
||||
|
||||
## Related
|
||||
|
||||
- [Add-ons overview](/whp/add-ons/overview/)
|
||||
|
||||
## Still stuck?
|
||||
|
||||
<Support />
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: Add-ons overview
|
||||
description: Optional features you can layer on your hosting plan — monitoring, archival email, resource upgrades, and more.
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
Add-ons are **optional extras** you can layer onto your base hosting plan. Each is billed separately and can be enabled or removed from your client portal at any time.
|
||||
|
||||
<Aside type="note">
|
||||
An add-on is billed in addition to your base plan. Removing an add-on stops the charge from the next billing cycle.
|
||||
</Aside>
|
||||
|
||||
## What's available
|
||||
|
||||
- **[Site Monitoring](/whp/add-ons/monitoring/)** — proactive alerts when something looks wrong (error spikes, brute-force attempts, exploit signatures).
|
||||
- **[Archival email](/whp/add-ons/archival-email/)** — long-term, searchable retention of your mail outside the live mailbox.
|
||||
- **[Resource upgrades](/whp/add-ons/resource-upgrades/)** — extra CPU, RAM, or storage without changing plans.
|
||||
- **[Email upgrades](/whp/add-ons/email-upgrades/)** — extra mailboxes or larger per-mailbox storage caps.
|
||||
|
||||
## How billing works
|
||||
|
||||
Add-ons are **pro-rated** to align with your base plan's billing cycle and appear as separate line items on your next invoice. Cancelling an add-on stops it from the next cycle — you keep access through the end of the period you've already paid for.
|
||||
|
||||
## How to enable an add-on
|
||||
|
||||
1. Sign in to the [client portal](https://secure.anhonesthost.com/clientarea.php).
|
||||
2. Go to **Services → My Services** and pick your hosting plan.
|
||||
3. Click **Upgrade/Downgrade** (sometimes called **Add-ons** depending on the add-on).
|
||||
4. Pick the add-on, review the price, and confirm.
|
||||
|
||||
WHP applies the change usually within a few minutes — no downtime for any of the add-ons listed here.
|
||||
|
||||
## Still stuck?
|
||||
|
||||
<Support />
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: Resource upgrades
|
||||
description: Add CPU, RAM, or disk to your container without migrating to a different plan.
|
||||
sidebar:
|
||||
order: 4
|
||||
---
|
||||
|
||||
import PaidAddon from '~/content/partials/paid-addon-callout.mdx';
|
||||
import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
<PaidAddon />
|
||||
|
||||
## What it does
|
||||
|
||||
Boost your container's **CPU**, **RAM**, or **persistent disk** without migrating to a new plan. Useful when you've outgrown your base allocation but the next plan tier up is more than you actually need.
|
||||
|
||||
## What you can upgrade
|
||||
|
||||
Each resource is independent — upgrade only what you need.
|
||||
|
||||
- **CPU cores per container** — visible on the Sites page as **CPU per Container (cores)** (default `0.25`). Add cores for sustained-compute workloads.
|
||||
- **RAM per container** — visible on the Sites page as **Memory per Container (MB)** (default `256`). For memory-hungry apps (caches, large WordPress sites with many plugins, Node apps holding big in-memory state).
|
||||
- **Number of containers** — scale a busy site horizontally from 1 up to 10 replicas; WHP load-balances traffic across them.
|
||||
- **Persistent disk** — for sites that store a lot of files (media libraries, user uploads, etc.).
|
||||
|
||||
## When you might need this
|
||||
|
||||
- **High CPU on busy days.** Sustained-load sites consistently hit the per-container CPU cap during peak hours.
|
||||
- **"Out of memory" errors** in your app log.
|
||||
- **Disk usage approaching 80%** of your allocation. Get ahead of it — full disks cause backup failures and uploads to fail.
|
||||
|
||||
## How to enable
|
||||
|
||||
For per-container CPU/RAM upgrades, edit the values directly on the **Sites** page for the site you want to change, and click **Save**. For account-wide upgrades (more total RAM, more total disk), open the [client portal](https://secure.anhonesthost.com/clientarea.php), go to your hosting service → **Upgrade/Downgrade**, and pick the resource increment.
|
||||
|
||||
## See your current usage
|
||||
|
||||
Open the **Dashboard** page in WHP. The **Server Information** and account stats show what you're consuming. For per-site usage, open a site from the **Sites** page.
|
||||
|
||||
## Related
|
||||
|
||||
- [Site Monitoring](/whp/add-ons/monitoring/) — catches resource saturation before customers complain.
|
||||
- [Add-ons overview](/whp/add-ons/overview/)
|
||||
|
||||
## Still stuck?
|
||||
|
||||
<Support />
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
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.
|
||||
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 Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
<SuperAdmin />
|
||||
|
||||
<Draft />
|
||||
|
||||
[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
|
||||
|
||||
The WAF runs in one of three modes, set per-site or server-wide:
|
||||
|
||||
- **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.
|
||||
|
||||
The WAF is fail-open: if the Coraza sidecar itself is unhealthy, traffic still flows.
|
||||
|
||||
## Sign in to WHP
|
||||
|
||||
<SignIn />
|
||||
|
||||
## Where it lives
|
||||
|
||||
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.
|
||||
|
||||
## Common tasks
|
||||
|
||||
### Roll a new site onto the WAF
|
||||
|
||||
<Aside type="tip">
|
||||
Always start in **Detect-only** for a new site. Move to **Enforce** only after you've watched the logs and confirmed no legitimate traffic is being matched.
|
||||
</Aside>
|
||||
|
||||
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**.
|
||||
|
||||
### Mute a noisy rule
|
||||
|
||||
When a rule is firing on legitimate traffic for one site:
|
||||
|
||||
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.
|
||||
|
||||
### 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:
|
||||
|
||||
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.
|
||||
3. Decide whether to mute the rule (see above) or leave it — many "false positives" turn out to be real attempts.
|
||||
|
||||
## Things to know
|
||||
|
||||
- **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.
|
||||
|
||||
## 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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
## Related
|
||||
|
||||
- [Server settings & services](/whp/admin/server-settings/)
|
||||
- [Site Monitoring rules](/whp/admin/site-monitoring/)
|
||||
|
||||
## Still stuck?
|
||||
|
||||
<Support />
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Admin overview
|
||||
description: What WHP super admin unlocks — server-wide controls for services, security rules, 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';
|
||||
|
||||
<SuperAdmin />
|
||||
|
||||
<Draft />
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Related
|
||||
|
||||
- [What is containerized hosting?](/whp/getting-started/what-is-containerized-hosting/) — the differences between container plans and full server access.
|
||||
|
||||
## Still stuck?
|
||||
|
||||
<Support />
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Server settings & services
|
||||
description: Restart Apache, PHP-FPM, MySQL; manage PHP modules and runtimes; edit server-wide configuration.
|
||||
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 Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
<SuperAdmin />
|
||||
|
||||
<Draft />
|
||||
|
||||
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
|
||||
|
||||
<SignIn />
|
||||
|
||||
## Restarting services
|
||||
|
||||
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.
|
||||
|
||||
Each service has a status indicator and **Restart**, **Stop**, and **Start** controls. Restart is the safe default for picking up new configuration.
|
||||
|
||||
<Aside type="caution">
|
||||
Restarting a service drops in-flight connections. For Apache and PHP-FPM, this means active web requests will fail to complete; for MySQL, in-flight transactions roll back. Pick a low-traffic window if you can.
|
||||
</Aside>
|
||||
|
||||
## Managing PHP modules and runtimes
|
||||
|
||||
WHP can run multiple PHP runtimes side-by-side (PHP 8.3, 8.4, etc.). With super admin you can:
|
||||
|
||||
- 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.
|
||||
|
||||
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.
|
||||
|
||||
## Server-wide configuration files
|
||||
|
||||
For settings that aren't exposed in the panel, you can SSH to the server and edit configuration directly:
|
||||
|
||||
- **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.
|
||||
|
||||
After editing, reload the relevant service from the WHP **Services** page (or via systemctl on the box).
|
||||
|
||||
## Common admin tasks
|
||||
|
||||
**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.
|
||||
|
||||
**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 <service>` on the box for the underlying message. 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/` instead, or open a ticket about adding a stable include hook.
|
||||
|
||||
## Related
|
||||
|
||||
- [Coraza WAF rules](/whp/admin/coraza-waf/)
|
||||
- [Users & delegated access](/whp/admin/user-management/)
|
||||
|
||||
## Still stuck?
|
||||
|
||||
<Support />
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: Site Monitoring rules
|
||||
description: Configure Site Monitoring across every site on the server — global ignore lists, alert routing, and severity tuning.
|
||||
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 Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
<SuperAdmin />
|
||||
|
||||
<Draft />
|
||||
|
||||
[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 to WHP
|
||||
|
||||
<SignIn />
|
||||
|
||||
## Where it lives
|
||||
|
||||
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.
|
||||
|
||||
## Common tasks
|
||||
|
||||
### Add a global ignore rule
|
||||
|
||||
When a signature is noisy for every site (for example, a known scanner you allow on your own infrastructure):
|
||||
|
||||
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.
|
||||
|
||||
<Aside type="caution">
|
||||
Global ignore rules silence every site on the server. Prefer per-site ignore (set in the customer-facing Site Monitoring page) when the noise is site-specific.
|
||||
</Aside>
|
||||
|
||||
### Tune severity for a rule
|
||||
|
||||
If a rule is set to **critical** but you've decided it's really informational in your environment:
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## 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).
|
||||
|
||||
**SMS not firing on critical.** Confirm SMS is enabled in **Alert Routing** and the phone number is verified.
|
||||
|
||||
## 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.
|
||||
|
||||
## Still stuck?
|
||||
|
||||
<Support />
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Users & delegated access
|
||||
description: Create sub-users, delegate panel access, and manage SFTP/SSH users at the server level.
|
||||
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 Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
<SuperAdmin />
|
||||
|
||||
<Draft />
|
||||
|
||||
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.
|
||||
|
||||
## Three kinds of access
|
||||
|
||||
| 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
|
||||
|
||||
<SignIn />
|
||||
|
||||
## Create a WHP sub-user
|
||||
|
||||
<Aside type="tip">
|
||||
WHP sub-users are the right model when someone needs to manage parts of the server alongside you. Use **delegated access** instead if they only need one site, or **SFTP/SSH** if they only need files.
|
||||
</Aside>
|
||||
|
||||
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.
|
||||
|
||||
## Delegated access for a single site
|
||||
|
||||
Use the customer-facing **Delegated Users** page (sidebar → **Delegated Users**, available on every account) when a contractor only needs to work on one site:
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
<Aside type="caution">
|
||||
Interactive SSH is powerful — anyone with it can run arbitrary commands inside the container or VM. Restrict to SFTP-only unless they specifically need a shell.
|
||||
</Aside>
|
||||
|
||||
## Managing existing users
|
||||
|
||||
The user list shows last sign-in, role, and 2FA status. Common actions from each user's row:
|
||||
|
||||
- **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.
|
||||
|
||||
## 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.
|
||||
|
||||
**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 `/`.
|
||||
|
||||
**A delegated user can't see DNS records.** Delegated access defaults to file-only. Edit their permissions to include DNS Management.
|
||||
|
||||
## Related
|
||||
|
||||
- [Server settings & services](/whp/admin/server-settings/)
|
||||
|
||||
## Still stuck?
|
||||
|
||||
<Support />
|
||||
@@ -9,7 +9,11 @@ Containerized hosting means your site lives in its own lightweight, isolated env
|
||||
|
||||
## An analogy
|
||||
|
||||
Think of a shared house with one kitchen versus an apartment building. **Shared hosting** is the house: lots of people share the same kitchen, and when one person leaves dishes in the sink, everyone notices. **Containerized hosting** gives you your own apartment, with your own kitchen, in the same building — you still benefit from the shared infrastructure (heat, security, plumbing), but day-to-day you're not stepping on anyone else's mess.
|
||||
Think of an **apartment** versus a **townhome in a gated community**.
|
||||
|
||||
**Shared hosting** is the apartment. You have your own unit, but the walls are thin. When the neighbour upstairs hosts a party, you hear it. When someone two doors down cooks something pungent, you smell it. You're sharing the same air, the same plumbing risers, the same elevator — so what one tenant does ripples through everyone else's day.
|
||||
|
||||
**Containerized hosting** is the townhome in a gated community. You still benefit from shared community resources — the gates, the security, the community centre, the maintained grounds — but you have your own walls, your own kitchen, your own front door. Whatever the neighbouring townhome does inside their walls stays inside their walls. You only feel the upside of being in the community, not the noise.
|
||||
|
||||
## How it compares to shared hosting
|
||||
|
||||
@@ -18,21 +22,24 @@ Think of a shared house with one kitchen versus an apartment building. **Shared
|
||||
- **Isolation = security.** A vulnerability in another customer's site can't reach yours. There's a hard boundary between containers.
|
||||
- **Add resources without moving.** Need more RAM next month? Upgrade in place — no migration, no downtime.
|
||||
|
||||
## How it compares to a VPS
|
||||
## How it compares to a Virtual Dedicated Server (VDS)
|
||||
|
||||
- **No server admin required.** We patch the underlying OS, monitor the host, and back up the underlying disk. You focus on your site, not on `apt update`.
|
||||
- **Right-sized resources.** A VPS gives you a whole virtual machine whether you need it or not. A container gives your site exactly the CPU, RAM, and disk you actually use — and you can resize whenever.
|
||||
- **Cheaper to run small.** Containers share the host efficiently, so you don't pay for an idle VM sitting at 5% CPU all day.
|
||||
A [Virtual Dedicated Server](https://anhonesthost.com/vds) is our VPS-class product — you get a whole virtual machine with admin access, and the same WHP control panel layered on top with full server-wide controls. The differences from a containerized plan:
|
||||
|
||||
- **No server admin required on a container plan.** We patch the underlying OS, monitor the host, and manage the services. On a VDS, you have admin access and can patch, configure, and manage server-wide settings yourself if you want to.
|
||||
- **Right-sized resources on a container plan.** A VDS gives you a whole virtual machine with fixed CPU/RAM/storage. A container gives your site exactly the CPU, RAM, and disk you actually use — and you can resize whenever.
|
||||
- **Cheaper to run small on a container plan.** Containers share the host efficiently, so you don't pay for an idle VM sitting at 5% CPU all day.
|
||||
|
||||
## What it doesn't change
|
||||
|
||||
- **SFTP still works** within the limits of your plan, so your usual file-upload workflow is unaffected.
|
||||
- **Your databases still live next to your site**, on the same host, with low-latency connections.
|
||||
- **Your domain works the same way** — DNS, SSL, redirects all behave like any other web host.
|
||||
- **The WHP panel works the same way.** Whether you're on a containerized plan or a VDS, you sign in to the same WHP — VDS just unlocks the server-wide admin sections.
|
||||
|
||||
## When you might want a VPS instead
|
||||
## When you might want a VDS instead
|
||||
|
||||
We'll be honest with you: if you need kernel-level tweaks, custom networking, non-standard runtimes we don't support, or root over SSH, a VPS may suit you better. Most sites don't need any of that — and the trade-off is that you're then responsible for your own OS upkeep.
|
||||
A VDS suits you when you want full admin control of the server — kernel-level tweaks, custom services, your own systemd units, or you simply prefer to manage the OS yourself. You get a real virtual machine with root access, and WHP still gives you the friendly panel on top.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -27,35 +27,41 @@ import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
<Steps>
|
||||
|
||||
1. From the sidebar, open **Domains → Add Domain**.
|
||||

|
||||
1. In the sidebar, click **Domains**.
|
||||

|
||||
|
||||
2. Enter your domain (for example, `example.com`). Don't include `http://` or `www.` — just the bare domain.
|
||||
2. Under **Add New Domain** on the left, type your domain (for example, `example.com`). Don't include `http://` or `www.` — just the bare domain.
|
||||
|
||||
3. Choose the domain type:
|
||||
- **Primary** — your account's main domain. Pick this if it's your first.
|
||||
- **Add-on** — an extra domain alongside your primary.
|
||||
|
||||
4. Click **Add**. WHP confirms the domain and shows the nameserver values and an A record you can point at.
|
||||
3. Click **Add Domain**.
|
||||
|
||||
</Steps>
|
||||
|
||||
WHP creates the standard set of DNS records automatically for the new domain:
|
||||
|
||||
- **A record** for the apex domain → your server's IP
|
||||
- **CNAME** for `www` → the apex
|
||||
- **NS records** for the nameservers
|
||||
- **MX record** → the mail server
|
||||
- **TXT record** for SPF
|
||||
|
||||
You can review and tweak any of these from the **DNS Management** panel on the right side of the Domains page (select the domain from the dropdown).
|
||||
|
||||
## Point your DNS at us
|
||||
|
||||
There are two paths depending on where the domain is registered:
|
||||
|
||||
**Registered with us:** nothing to do. We set DNS automatically when the domain was created. Skip ahead to verify.
|
||||
**Registered with us.** Nothing to do — we set DNS automatically when the domain was added. Skip ahead to verify.
|
||||
|
||||
**Registered elsewhere:** at your registrar, do one of the following:
|
||||
**Registered elsewhere.** At your registrar, do one of the following:
|
||||
|
||||
- Set the **nameservers** to `ns1.anhonesthost.com` and `ns2.anhonesthost.com` (recommended — gives us full DNS control, easier to support), **or**
|
||||
- Keep your existing nameservers and add an **A record** pointing the domain to the IP address shown on the WHP confirmation screen.
|
||||
- Set the **nameservers** to the values shown on the Domains page (recommended — gives us full DNS control, easier to support), **or**
|
||||
- Keep your existing nameservers and add an **A record** pointing the domain to the IP shown on the **Dashboard** page under Server Information.
|
||||
|
||||
## Verify it worked
|
||||
|
||||
DNS changes can take up to 24 hours to propagate worldwide. To check status:
|
||||
DNS changes can take up to 24 hours to propagate worldwide. To check:
|
||||
|
||||
- Run `dig example.com +short` from a terminal — once you see our IP in the result, you're live.
|
||||
- Run `dig example.com +short` from a terminal — once you see our server's IP in the result, you're live.
|
||||
- Or use a web tool like [whatsmydns.net](https://www.whatsmydns.net/) to see propagation across many regions.
|
||||
|
||||
Once DNS resolves, visiting your domain in a browser will reach WHP — though you'll see a default placeholder until you create a site.
|
||||
@@ -64,9 +70,9 @@ Once DNS resolves, visiting your domain in a browser will reach WHP — though y
|
||||
|
||||
**"Pending DNS" for more than 24 hours.** Re-check the nameservers or A record at your registrar — typos and trailing dots cause silent failures. Clear your local DNS cache to rule out client-side caching.
|
||||
|
||||
**Domain returns a generic "site not found" page.** DNS works but you haven't created a site yet. [Create one](/whp/how-to/create-a-site/) and point it at this domain.
|
||||
**Domain returns a generic placeholder.** DNS works but you haven't created a site yet. [Create one](/whp/how-to/create-a-site/) and bind this domain to it.
|
||||
|
||||
**Wrong site loads.** Another site on your account is set as the default. In **Sites**, make sure the correct site is bound to this domain.
|
||||
**Wrong site loads.** Another site on your account is bound to this domain. Open **Sites** in the sidebar and make sure the correct site is bound to it.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Backups
|
||||
description: Confirm WHP is backing up your site, restore individual files, and download archives.
|
||||
description: Run on-demand and scheduled backups of your sites and databases, and confirm they're succeeding.
|
||||
sidebar:
|
||||
order: 4
|
||||
---
|
||||
@@ -9,76 +9,79 @@ import { Steps, Aside } from '@astrojs/starlight/components';
|
||||
import SignIn from '~/content/partials/signing-in.mdx';
|
||||
import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
WHP runs **automatic daily backups** of your site files and databases. You don't have to set anything up for this to happen — but it's worth knowing how to confirm a backup ran, restore something, and download an archive when you need to.
|
||||
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.
|
||||
|
||||
## What's backed up by default
|
||||
## What's backed up
|
||||
|
||||
- **Site files** — everything under each domain folder.
|
||||
- **Databases** — all MySQL databases attached to your account.
|
||||
- **Email** — mailboxes and their contents (depending on your plan).
|
||||
- **Sites** — files for each site.
|
||||
- **Databases** — every MySQL and PostgreSQL database attached to your account.
|
||||
|
||||
Retention is shown on your plan page. The default tier keeps **7 daily backups**.
|
||||
Default retention on built-in backup targets is **5 days, up to 10 backups**.
|
||||
|
||||
## Sign in to WHP
|
||||
|
||||
<SignIn />
|
||||
|
||||
## Configure your backup schedule
|
||||
## Run an on-demand backup
|
||||
|
||||
<Steps>
|
||||
|
||||
1. From the sidebar, open **Backups → Settings**.
|
||||

|
||||
1. In the sidebar, click **Backups**.
|
||||

|
||||
|
||||
2. Confirm the **schedule** (default: daily, overnight). Adjust if your plan permits.
|
||||
2. Under **Create New Backup**, pick a **Backup Type** (Sites, Databases, or both) and a **Backup Target** from the dropdown.
|
||||
|
||||
3. (Optional) Enable **off-server backups** to a separate storage location — recommended for anything you can't afford to lose.
|
||||
3. Click **Start Backup**. The run appears in **Backup History** below; status updates live.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Verify a backup actually ran
|
||||
## Schedule a backup
|
||||
|
||||
<Steps>
|
||||
|
||||
1. Open **Backups → History**.
|
||||

|
||||
1. Scroll down to **Scheduled Backups** and click **+ Add Schedule**.
|
||||
|
||||
2. Confirm the most recent entry is from **within the last 24 hours** and shows **Success**.
|
||||
2. Pick the cadence (daily, weekly, etc.), the type (sites / databases / both), and the target.
|
||||
|
||||
3. Click the entry to see what was included — files, databases, email — and the total size.
|
||||
3. Save. The schedule appears in the list and runs automatically.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Where backups are stored
|
||||
|
||||
The **Backup Targets** table shows the destinations available to your account. Built-in targets are S3-backed (for example, `WHP01 S3 Backups`) with retention and a maximum backup count. **Global** targets are managed by us; you can also add your own external target (for example, your own S3 bucket) with **+ Add Backup Target**.
|
||||
|
||||
## Verify backups are succeeding
|
||||
|
||||
<Steps>
|
||||
|
||||
1. Open **Backups**.
|
||||
|
||||
2. Look at the **Total Backups** and **Total Size** tiles at the top. If Total Backups stays at 0 over time, no backups are running — open a [support ticket](https://secure.anhonesthost.com/submitticket.php).
|
||||
|
||||
3. Check **Backup History** for recent entries. Each should show **Success** within minutes of its scheduled time.
|
||||
|
||||
</Steps>
|
||||
|
||||
<Aside type="tip">
|
||||
Add a calendar reminder to check this monthly. Backups that fail silently are the worst kind.
|
||||
Add a calendar reminder to check the history monthly. Backups that fail silently are the worst kind.
|
||||
</Aside>
|
||||
|
||||
## Test a restore (recommended quarterly)
|
||||
|
||||
The best time to discover your backup isn't working is *not* when you actually need it. Test surgically:
|
||||
|
||||
1. In **History**, click a recent backup → **Restore preview**.
|
||||
2. Pick a single file — your site's `index.php`, for example — and restore just that one file.
|
||||
3. Confirm it appears correctly. If yes, the restore pipeline works.
|
||||
Find the time to do this before you actually need it. Open a recent backup in **Backup History** → preview the contents → restore a single file (your site's `index.php` is a fine target) → confirm it appears. If the surgical restore works, the pipeline works.
|
||||
|
||||
Don't do a full restore unless you genuinely need to — it rewrites your live site.
|
||||
|
||||
## Download a backup
|
||||
|
||||
To grab a copy off-server:
|
||||
|
||||
1. From **History**, click a backup → **Download**.
|
||||
2. WHP packages it as `.tar.gz` and offers a one-time download link that's valid for 24 hours.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Backup failed.** Click the failed row to see the error. Common causes:
|
||||
|
||||
- Disk full or close to it — check **Overview → Resource usage** and consider a [resource upgrade](/whp/add-ons/resource-upgrades/).
|
||||
- A very large mailbox slowed the run — consider the [archival email add-on](/whp/add-ons/archival-email/) to relieve mailbox pressure.
|
||||
- Transient lock or maintenance window — let it retry overnight before opening a ticket.
|
||||
- Transient lock or maintenance window — let it retry on the next schedule before opening a ticket.
|
||||
|
||||
**Restore "succeeded" but my file isn't there.** Check the **path** shown on the backup's detail page — your restore landed where the backup says it would, which may not match your current site layout if you've moved files around.
|
||||
**"No targets available" when starting a backup.** Your account has no backup targets attached. Open a ticket; we'll get one wired up.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Create a site
|
||||
description: Spin up a PHP, Node, or static HTML site on a domain you've added to WHP.
|
||||
description: Spin up a containerized site on a domain you've added to WHP.
|
||||
sidebar:
|
||||
order: 2
|
||||
---
|
||||
@@ -10,10 +10,12 @@ import SignIn from '~/content/partials/signing-in.mdx';
|
||||
import Hostnames from '~/content/partials/service-hostnames.mdx';
|
||||
import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
Every site on WHP runs in one or more containers. The Sites page lets you pick a container type (PHP, Node, static HTML, or one of the other options on the dropdown), bind one or more domains, and set how much CPU and RAM each container gets.
|
||||
|
||||
## Before you start
|
||||
|
||||
- A **domain already added** to your account ([Add a domain](/whp/how-to/add-a-domain/) covers that).
|
||||
- Decide what kind of site you're building: **PHP**, **Node**, or **static HTML**.
|
||||
- Decide what kind of site you're building. Common container types: **PHP** (WordPress, Laravel, most CMSes), **Node** (custom apps), **Static HTML** (prebuilt bundles from Astro, Hugo, Eleventy, etc.).
|
||||
- About 5 minutes.
|
||||
|
||||
## Sign in to WHP
|
||||
@@ -24,30 +26,30 @@ import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
<Steps>
|
||||
|
||||
1. From the sidebar, open **Sites → Add Site**.
|
||||

|
||||
1. In the sidebar, click **Sites**.
|
||||

|
||||
|
||||
2. Pick the **site type**:
|
||||
- **PHP** — best for WordPress, Joomla, Laravel, and most off-the-shelf CMSes.
|
||||
- **Node** — for custom apps you build and deploy (Express, Next.js in standalone mode, Fastify, etc.).
|
||||
- **Static HTML** — for prebuilt site bundles from tools like Astro, Hugo, Eleventy, or Jekyll. No runtime; just files.
|
||||
2. Fill in the **Create New Site** form on the right:
|
||||
- **Site Name** — a friendly label for you to recognise the site later.
|
||||
- **Primary Domain or Subdomain** — pick from the dropdown of domains you've already added.
|
||||
- **Container Type** — PHP, Node, static HTML, etc.
|
||||
- **Number of Containers** — `1` is the right answer for most sites. Bump this up (1–10) if you need to scale a busy site horizontally; WHP load-balances traffic across the replicas.
|
||||
- **Additional Domains/Subdomains (Optional)** — bind extra domains to the same site if you want them to serve the same content.
|
||||
- **CPU per Container (cores)** — defaults to `0.25`. Raise this only if you need to.
|
||||
- **Memory per Container (MB)** — defaults to `256`. Raise this if your app needs more RAM.
|
||||
|
||||
3. Pick the **domain**. The dropdown lists domains you've already added.
|
||||
3. Scroll down to **SSL/HAProxy Configuration**. Before you enable HTTPS, make sure your domain's DNS already points to our server's IP — SSL certificates can't be issued for domains that aren't pointed correctly. Then check **Enable HTTPS** to have us request a Let's Encrypt certificate.
|
||||
|
||||
4. (PHP only) Pick the **PHP version**.
|
||||
|
||||
5. (Node only) Pick the **Node version** and the **start command** (typically `npm run start`).
|
||||
|
||||
6. Click **Create site**. WHP provisions the container — usually under a minute. You'll see the new site in the **Sites** list when it's ready.
|
||||
4. Click **Create Site**. WHP provisions the container(s) — usually under a minute. The new site shows up in the **Manage Sites** list on the left of the page.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Where your files go
|
||||
|
||||
When you connect via SFTP, your account home shows one folder per domain. Inside each domain folder, the layout depends on the site type:
|
||||
When you connect via SFTP, your account home (`/docker/users/<your-user>/`) shows one folder per domain. Inside each domain folder, the layout depends on the container type:
|
||||
|
||||
| Site type | Layout | Docroot is... |
|
||||
| ------------ | ----------------------------------------------------- | ------------------------------------------------------ |
|
||||
| Container type | Layout inside the domain folder | Docroot is... |
|
||||
| -------------- | ------------------------------------------ | -------------------------------------------------------- |
|
||||
| PHP | `public_html/`, `logs/`, `crontab` | `public_html/` |
|
||||
| Node | `app/`, `logs/` | Whatever your start command serves; we run it from `app/`|
|
||||
| Static HTML | files directly (e.g., `index.html`, `assets/`) | The domain folder itself |
|
||||
@@ -63,16 +65,18 @@ Upload your files to the right place and the site picks them up immediately.
|
||||
Open your domain in a browser. You should see your site, or the default "no content yet" placeholder if you haven't uploaded files. For Node sites, give it 15–30 seconds after creation for the process to start.
|
||||
|
||||
<Aside type="tip">
|
||||
Static HTML sites are the fastest path if your toolchain produces a bundle (Astro, Hugo, Eleventy, etc.) — you skip the runtime entirely.
|
||||
Static HTML is the fastest path if your toolchain produces a bundle (Astro, Hugo, Eleventy, etc.) — you skip the runtime entirely.
|
||||
</Aside>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**You see a default "no content" page.** Upload your files via SFTP — for PHP into `public_html/`, for Node into `app/`, for static directly into the domain folder.
|
||||
|
||||
**Node site returns 502.** Check that your start command runs successfully locally, and that it binds to the port shown on the site's WHP detail page (we set it via an env var; see the site's environment in WHP).
|
||||
**Node site returns 502.** Check that your start command runs successfully locally, and that it binds to the port WHP exposes via env var to your container.
|
||||
|
||||
**PHP site shows a blank page or 500.** Check your site's error log (sidebar → Logs → pick the site). Common cause: a syntax error in `index.php` or a missing PHP extension — request extensions via a [support ticket](https://secure.anhonesthost.com/submitticket.php) if needed.
|
||||
**PHP site shows a blank page or 500.** Check **Sites → your site → Logs** (or the `logs/` folder in SFTP). Common cause: a syntax error in `index.php` or a missing PHP extension — open a [support ticket](https://secure.anhonesthost.com/submitticket.php) if you need a non-default extension enabled.
|
||||
|
||||
**SSL won't issue.** The "Important: ensure your domain is pointed to our server" warning means DNS hasn't propagated yet, or it's pointed somewhere else. Re-check with `dig` and retry.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
## Before you start
|
||||
|
||||
- A **domain added** to your account.
|
||||
- A **domain added** to your account ([Add a domain](/whp/how-to/add-a-domain/) covers that).
|
||||
- Decide what local part you want — the bit before the `@`. For example, `jane` to get `jane@example.com`.
|
||||
- About 5 minutes.
|
||||
|
||||
@@ -23,60 +23,58 @@ import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
<Steps>
|
||||
|
||||
1. From the sidebar, open **Email → Email Accounts → Add Account**.
|
||||

|
||||
1. In the sidebar, click **Email**.
|
||||

|
||||
|
||||
2. Pick the **domain** for this mailbox.
|
||||
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.
|
||||
|
||||
3. Enter the **local part** (the bit before `@`).
|
||||
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. 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.
|
||||
|
||||
5. Choose a **mailbox size cap**. The default is usually fine; you can raise it later or via an [email upgrade add-on](/whp/add-ons/email-upgrades/).
|
||||
|
||||
6. Click **Create**.
|
||||
4. Click **Create**. The new account appears in the **Email Accounts** list.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Set up your email client
|
||||
|
||||
Use these settings in Outlook, Apple Mail, Thunderbird, or any other client:
|
||||
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.
|
||||
|
||||
The typical settings look like this; substitute the hostname shown in the Setup Instructions:
|
||||
|
||||
```text
|
||||
IMAP (incoming)
|
||||
Host: mail.<yourdomain>
|
||||
Host: <see Setup Instructions>
|
||||
Port: 993
|
||||
Security: SSL/TLS
|
||||
Username: full email address (e.g., jane@example.com)
|
||||
Password: the one you set above
|
||||
|
||||
SMTP (outgoing)
|
||||
Host: mail.<yourdomain>
|
||||
Host: <see Setup Instructions>
|
||||
Port: 587
|
||||
Security: STARTTLS
|
||||
Username: full email address
|
||||
Password: same as IMAP
|
||||
```
|
||||
|
||||
For per-client walkthroughs (Outlook, Apple Mail, 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
|
||||
|
||||
You can also access the mailbox from a browser at `https://webmail.<yourdomain>` once DNS for the mail subdomain has propagated.
|
||||
Click **Webmail Access → Open Webmail** on the Email page to sign in to webmail in a new tab.
|
||||
|
||||
## 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.
|
||||
|
||||
<Aside type="caution">
|
||||
**SPF and DKIM records matter.** Without them, your outgoing mail will get flagged or rejected by other providers. If your domain is registered with us, we add these records automatically. If it's registered elsewhere, copy the SPF and DKIM records shown on the email account page into your registrar's DNS.
|
||||
**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.
|
||||
</Aside>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Webmail isn't reachable.** DNS for the `mail.<yourdomain>` subdomain may still be propagating — wait an hour and try again.
|
||||
**Webmail isn't reachable.** DNS for the mail subdomain may still be propagating — wait an hour and try again.
|
||||
|
||||
**Outgoing mail is bouncing or going to spam.** Check the SPF and DKIM records at your registrar. The email account page in WHP shows the exact records you should have.
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: Service hostnames
|
||||
description: Quick reference for connecting to MySQL, PostgreSQL, and Valkey from inside your container.
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
import Hostnames from '~/content/partials/service-hostnames.mdx';
|
||||
import Support from '~/content/partials/support-link.mdx';
|
||||
|
||||
Use these hostnames from inside your WHP container — they resolve to the internal services your site can talk to. They don't work from your laptop; see the note below.
|
||||
|
||||
<Hostnames />
|
||||
|
||||
## Where do I get credentials?
|
||||
|
||||
- **MySQL** — sidebar → **MySQL Management**. Create users and databases there; the **phpMyAdmin** link below it opens an SSO'd phpMyAdmin in a new tab.
|
||||
- **PostgreSQL** — sidebar → **PostgreSQL**. Same shape as MySQL Management; the **Adminer** link below it opens an SSO'd Adminer.
|
||||
- **Valkey** — sidebar → **Valkey Cache**. Click **Enable Valkey**, pick a memory cap and mode, and you're done — no password needed because connections are network-isolated to your account.
|
||||
|
||||
## Mail servers
|
||||
|
||||
Mail server hostnames are not per-domain. Your server has one mail server hostname (for example `mail01.cloud-hosting.io`) that handles IMAP, POP3, and SMTP for every domain on that server. Find your exact mail server hostname on the **Dashboard** page in WHP under **Server Information** — that's also where you'll find your server's IP, primary nameservers, and your home directory.
|
||||
|
||||
For step-by-step email-client setup, the Email page has a **Setup Instructions** button that lays out the exact host, port, and security settings.
|
||||
|
||||
## Connecting from your laptop
|
||||
|
||||
Direct external connections to these internal services are not exposed. Use the panel's tools instead:
|
||||
|
||||
- **MySQL** — **phpMyAdmin** in the panel for ad-hoc queries.
|
||||
- **PostgreSQL** — **Adminer** in the panel.
|
||||
- **Valkey** — there's no first-party browser tool; use a Valkey/Redis client inside your site's **Terminal** (sidebar → **Terminal**).
|
||||
|
||||
## Related
|
||||
|
||||
- [Create a site](/whp/how-to/create-a-site/)
|
||||
- [Create an email account](/whp/how-to/create-an-email-account/)
|
||||
|
||||
## Still stuck?
|
||||
|
||||
<Support />
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
<Aside type="caution" title="Draft — work in progress">
|
||||
This page is a draft. Some details may not match the panel yet, and screenshots are still pending. We're publishing it as a placeholder so the structure is in place; we'll fill in the gaps as we go.
|
||||
</Aside>
|
||||
@@ -1,11 +1,11 @@
|
||||
Some applications need to connect to databases, caches, or other internal services. Use these hostnames from inside your site — they only resolve within your hosting container, so they don't work from your laptop.
|
||||
Some applications need to connect to databases or caches. Use these hostnames from inside your site — they only resolve within your hosting container, so they don't work from your laptop.
|
||||
|
||||
| Service | Hostname | Default port | Notes |
|
||||
| ---------------------- | ----------------------- | ------------ | ------------------------------------------------------ |
|
||||
| MySQL | `mysql` | 3306 | Username, password, and database from your WHP panel. |
|
||||
| PostgreSQL | `postgres` | 5432 | Available on most plans; ask support if it's missing. |
|
||||
| Memcached | `memcache` | 11211 | No auth; isolated per container. |
|
||||
| Redis | `redis` | 6379 | Single DB per site; password in panel. |
|
||||
| SMTP (outbound) | `smtp.anhonesthost.com` | 587 | Use your full email address as the username. |
|
||||
| ----------- | ----------- | ------------ | -------------------------------------------------------------------------------- |
|
||||
| MySQL | `mysql` | 3306 | Username, password, and database from the **MySQL Management** page. |
|
||||
| PostgreSQL | `postgres` | 5432 | Username, password, and database from the **PostgreSQL** page. |
|
||||
| Valkey | `valkey` | 6379 | Redis wire-compatible (phpredis, Predis, ioredis work unchanged). No password — the connection is network-isolated to your account. Enable on the **Valkey Cache** page first. |
|
||||
|
||||
For **email**, the mail server hostname is a per-server value (e.g. `mail01.cloud-hosting.io`) and is shown on the **Dashboard** page under Server Information.
|
||||
|
||||
**Connecting from your laptop?** Use the panel's **phpMyAdmin** (MySQL) or **Adminer** (PostgreSQL) for ad-hoc queries — direct external connections to these internal services are not exposed for security reasons.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="anhh-accent-callout">
|
||||
**Requires WHP super admin access.** These features are unlocked for customers with a WHP super admin role on the server — for example, anyone running a [Virtual Dedicated Server](https://anhonesthost.com/vds). Customers without super admin won't see these pages.
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
// Side-effect imports of CSS shipped by @fontsource-variable packages
|
||||
declare module '@fontsource-variable/*';
|
||||
declare module '@fontsource/*';
|
||||
@@ -75,10 +75,52 @@ const visibleProducts: Product[] = Array.from(byProduct.entries())
|
||||
}
|
||||
header,
|
||||
footer {
|
||||
padding: 1.5rem 2rem;
|
||||
padding: 1.25rem 2rem;
|
||||
}
|
||||
header {
|
||||
border-bottom: 1px solid var(--anhh-border-color, #334155);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
a.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.ext-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
.ext-link {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--anhh-text-secondary, #94a3b8);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ext-link:hover,
|
||||
.ext-link:focus {
|
||||
color: var(--anhh-accent, #00d4aa);
|
||||
outline: none;
|
||||
}
|
||||
@media (max-width: 40em) {
|
||||
.ext-nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.kb-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--anhh-text-secondary, #94a3b8);
|
||||
padding-left: 0.75rem;
|
||||
border-left: 1px solid var(--anhh-border-color, #334155);
|
||||
line-height: 1;
|
||||
}
|
||||
footer {
|
||||
border-top: 1px solid var(--anhh-border-color, #334155);
|
||||
@@ -152,7 +194,21 @@ const visibleProducts: Product[] = Array.from(byProduct.entries())
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><strong>An Honest Host</strong> · Knowledge Base</header>
|
||||
<header>
|
||||
<a href="/" class="brand" aria-label="AnHonestHost">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 32" fill="none" height="28" role="img">
|
||||
<path d="M12 4L3 16L12 28" stroke="#00d4aa" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M24 4L33 16L24 28" stroke="#00d4aa" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="12" y1="16" x2="24" y2="16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.35"/>
|
||||
<text x="44" y="21" font-family="Inter, system-ui, -apple-system, sans-serif" font-size="18" font-weight="800" fill="currentColor" letter-spacing="-0.5">AnHonest<tspan fill="#00d4aa">Host</tspan></text>
|
||||
</svg>
|
||||
<span class="kb-label">Knowledge Base</span>
|
||||
</a>
|
||||
<nav class="ext-nav" aria-label="AnHonestHost links">
|
||||
<a href="https://anhonesthost.com/" class="ext-link">Get a Plan</a>
|
||||
<a href="https://secure.anhonesthost.com/clientarea.php" class="ext-link">Client Portal</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<h1>Knowledge Base</h1>
|
||||
<p class="lede">Pick a product to get started.</p>
|
||||
|
||||
@@ -83,3 +83,24 @@
|
||||
body {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/*
|
||||
* Active sidebar item — Starlight default uses --sl-color-text-accent as the
|
||||
* background, which on our teal (#00d4aa) gave only ~3:1 contrast against the
|
||||
* dark gray text. Override with our primary blue + white text so contrast
|
||||
* exceeds WCAG AA in both modes.
|
||||
*
|
||||
* Starlight scopes its rules via Astro's data-astro-cid-* attributes, so we
|
||||
* need at least matching specificity. `:is(html)` adds a class-equivalent.
|
||||
*/
|
||||
/*
|
||||
* Specificity bump: customCss loads BEFORE Starlight's component-scoped CSS,
|
||||
* and Starlight's rule has specificity (0,2,0) via its scoped class. We use
|
||||
* a doubled attribute selector + html prefix for (0,2,1) to override.
|
||||
*/
|
||||
html [aria-current='page'][aria-current='page'],
|
||||
html [aria-current='page'][aria-current='page']:hover,
|
||||
html [aria-current='page'][aria-current='page']:focus {
|
||||
background-color: var(--anhh-primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Screenshot pipeline
|
||||
|
||||
Captures real WHP screenshots into `src/assets/screenshots/whp/`.
|
||||
|
||||
**Local-only. Never runs in CI.** CI builds the static site; it never opens a browser or hits WHP.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. A demo WHP account (prod recommended for accuracy; staging works internally).
|
||||
2. Network access to that WHP host.
|
||||
3. Node 20+ and Playwright Chromium installed: `npx playwright install chromium`.
|
||||
|
||||
## Configure
|
||||
|
||||
Create `tools/screenshots/.env` (gitignored):
|
||||
|
||||
```
|
||||
WHP_BASE=https://whp01.cloud-hosting.io:8443
|
||||
WHP_USER=demo-kb
|
||||
WHP_PASS=…
|
||||
```
|
||||
|
||||
The script reads these via `process.env`; pass them in your shell or use a `.env` loader like `dotenv-cli`.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
# Load .env into your shell (one option):
|
||||
set -a; source tools/screenshots/.env; set +a
|
||||
npm run screenshots
|
||||
```
|
||||
|
||||
Outputs one PNG per entry in `shots.config.ts` to `src/assets/screenshots/whp/<id>.png`. Existing files are overwritten.
|
||||
|
||||
## Capture rules
|
||||
|
||||
- **Viewport: 1440×900.** Locked. No `fullPage`. Playwright's viewport screenshots never include browser chrome — no address bar, no tab strip.
|
||||
- **Mask list:** defaults (account ID, server hostname, user IP, billing column) plus per-shot additions.
|
||||
- **No address bar in any image.** Multi-server fleet — we don't want a specific host in any screenshot.
|
||||
- Use `selector` to clip to a region (e.g., just the sidebar) when the full viewport is noisier than useful.
|
||||
|
||||
## Refresh workflow
|
||||
|
||||
UI changed? → `npm run screenshots` locally → review the diffs (`git diff --stat` shows changed PNGs) → eyeball them for accidental leakage → commit → push.
|
||||
|
||||
## Adding a new shot
|
||||
|
||||
1. Add an entry to `shots.config.ts` with a stable `id`.
|
||||
2. `npm run screenshots`.
|
||||
3. Reference the new file in your `.mdx`: ``.
|
||||
@@ -0,0 +1,98 @@
|
||||
[
|
||||
{
|
||||
"text": "Logout",
|
||||
"href": "index.php?whp-action=logout"
|
||||
},
|
||||
{
|
||||
"text": "Dashboard",
|
||||
"href": "index.php?page=dashboard"
|
||||
},
|
||||
{
|
||||
"text": "Sites",
|
||||
"href": "index.php?page=sites"
|
||||
},
|
||||
{
|
||||
"text": "Traffic",
|
||||
"href": "index.php?page=site-traffic"
|
||||
},
|
||||
{
|
||||
"text": "Site Builder BETA",
|
||||
"href": "index.php?page=site-builder"
|
||||
},
|
||||
{
|
||||
"text": "Domains",
|
||||
"href": "index.php?page=domains"
|
||||
},
|
||||
{
|
||||
"text": "WordPress",
|
||||
"href": "index.php?page=wordpress"
|
||||
},
|
||||
{
|
||||
"text": "File Manager",
|
||||
"href": "./filemanager/"
|
||||
},
|
||||
{
|
||||
"text": "Terminal",
|
||||
"href": "index.php?page=terminal"
|
||||
},
|
||||
{
|
||||
"text": "cPanel Import",
|
||||
"href": "index.php?page=cpanel-import"
|
||||
},
|
||||
{
|
||||
"text": "MySQL Management",
|
||||
"href": "index.php?page=database-management"
|
||||
},
|
||||
{
|
||||
"text": "phpMyAdmin",
|
||||
"href": "./phpmyadmin-sso.php"
|
||||
},
|
||||
{
|
||||
"text": "PostgreSQL",
|
||||
"href": "index.php?page=postgresql-management"
|
||||
},
|
||||
{
|
||||
"text": "Adminer",
|
||||
"href": "./adminer-sso.php"
|
||||
},
|
||||
{
|
||||
"text": "Valkey Cache",
|
||||
"href": "index.php?page=account-valkey"
|
||||
},
|
||||
{
|
||||
"text": "Email",
|
||||
"href": "index.php?page=email-management"
|
||||
},
|
||||
{
|
||||
"text": "Backups",
|
||||
"href": "index.php?page=backups"
|
||||
},
|
||||
{
|
||||
"text": "Site Monitoring",
|
||||
"href": "index.php?page=ai-monitor-customer"
|
||||
},
|
||||
{
|
||||
"text": "Delegated Users",
|
||||
"href": "index.php?page=delegated-users"
|
||||
},
|
||||
{
|
||||
"text": "Active Sessions",
|
||||
"href": "index.php?page=active-sessions"
|
||||
},
|
||||
{
|
||||
"text": "Cloud Apache Container",
|
||||
"href": "https://repo.anhonesthost.net/cloud-hosting-platform/cloud-apache-container"
|
||||
},
|
||||
{
|
||||
"text": "Cloud Node Container",
|
||||
"href": "https://repo.anhonesthost.net/cloud-hosting-platform/cloud-node-container"
|
||||
},
|
||||
{
|
||||
"text": "Manage Domains",
|
||||
"href": "index.php?page=domains"
|
||||
},
|
||||
{
|
||||
"text": "Manage Databases",
|
||||
"href": "index.php?page=database-management"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Discovery script — logs in, captures the dashboard, and probes the sidebar
|
||||
* nav to learn the actual URLs of the sections referenced in our docs.
|
||||
*
|
||||
* Not committed for ongoing use; the canonical capture is run.ts.
|
||||
*/
|
||||
import { chromium, type Page } from 'playwright';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const OUT_DIR = resolve(__dirname, '../../src/assets/screenshots/whp');
|
||||
|
||||
function need(name: string): string {
|
||||
const v = process.env[name];
|
||||
if (!v) throw new Error(`missing env: ${name}`);
|
||||
return v;
|
||||
}
|
||||
|
||||
const BASE = need('WHP_BASE');
|
||||
const USER = need('WHP_USER');
|
||||
const PASS = need('WHP_PASS');
|
||||
|
||||
async function login(page: Page): Promise<void> {
|
||||
console.log(`navigating ${BASE}/login.php`);
|
||||
await page.goto(`${BASE}/login.php`, { waitUntil: 'domcontentloaded' });
|
||||
console.log(`landed at ${page.url()}`);
|
||||
|
||||
await page.fill('input[name="user"]', USER);
|
||||
await page.fill('input[name="password"]', PASS);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState('networkidle');
|
||||
console.log(`logged in, now at ${page.url()}`);
|
||||
}
|
||||
|
||||
async function snapshotPage(page: Page, id: string): Promise<void> {
|
||||
await mkdir(OUT_DIR, { recursive: true });
|
||||
const path = resolve(OUT_DIR, `${id}.png`);
|
||||
await page.screenshot({ path, fullPage: false });
|
||||
console.log(`captured ${id} -> ${path}`);
|
||||
}
|
||||
|
||||
async function listNavLinks(page: Page): Promise<{ text: string; href: string | null }[]> {
|
||||
// Use locator API to collect all anchors with hrefs (no string-eval).
|
||||
const anchors = page.locator('a[href]');
|
||||
const count = await anchors.count();
|
||||
const out: { text: string; href: string | null }[] = [];
|
||||
for (let i = 0; i < count && out.length < 120; i++) {
|
||||
const a = anchors.nth(i);
|
||||
const href = await a.getAttribute('href');
|
||||
if (!href || href.startsWith('#')) continue;
|
||||
const text = ((await a.textContent()) ?? '').trim().slice(0, 60);
|
||||
if (!text) continue;
|
||||
out.push({ text, href });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function probe(page: Page, label: string, hrefHint: string): Promise<string | null> {
|
||||
const link = page.locator(`a[href*="${hrefHint}"]`).first();
|
||||
if ((await link.count()) === 0) {
|
||||
console.log(` NO MATCH for "${label}" (hint=${hrefHint})`);
|
||||
return null;
|
||||
}
|
||||
const href = await link.getAttribute('href');
|
||||
console.log(` ${label}: ${href}`);
|
||||
return href;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const ctx = await browser.newContext({
|
||||
ignoreHTTPSErrors: true,
|
||||
viewport: { width: 1440, height: 900 },
|
||||
});
|
||||
const page = await ctx.newPage();
|
||||
|
||||
try {
|
||||
await login(page);
|
||||
await snapshotPage(page, '_discovery-dashboard');
|
||||
|
||||
const navLinks = await listNavLinks(page);
|
||||
await writeFile(
|
||||
resolve(__dirname, '_discovered-nav.json'),
|
||||
JSON.stringify(navLinks, null, 2),
|
||||
);
|
||||
console.log(`wrote ${navLinks.length} nav links to _discovered-nav.json`);
|
||||
|
||||
for (const [label, hint] of [
|
||||
['Domains', 'domain'],
|
||||
['Sites', 'site'],
|
||||
['Email', 'email'],
|
||||
['Backups', 'backup'],
|
||||
['Monitor', 'monitor'],
|
||||
['Resources', 'resource'],
|
||||
['Dashboard', 'dashboard'],
|
||||
] as const) {
|
||||
await probe(page, label, hint);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* One-shot peek script: log in, navigate to a specific URL, screenshot it.
|
||||
* Usage: PEEK_URL=... npx tsx tools/screenshots/peek.ts
|
||||
*/
|
||||
import { chromium, type Page } from 'playwright';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const OUT_DIR = resolve(__dirname, '../../src/assets/screenshots/whp');
|
||||
|
||||
function need(name: string): string {
|
||||
const v = process.env[name];
|
||||
if (!v) throw new Error(`missing env: ${name}`);
|
||||
return v;
|
||||
}
|
||||
|
||||
const BASE = need('WHP_BASE');
|
||||
const USER = need('WHP_USER');
|
||||
const PASS = need('WHP_PASS');
|
||||
const PEEK = need('PEEK_URL');
|
||||
const ID = process.env.PEEK_ID ?? '_peek';
|
||||
|
||||
async function login(page: Page): Promise<void> {
|
||||
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 main(): Promise<void> {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
await login(page);
|
||||
await page.goto(`${BASE}${PEEK}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await mkdir(OUT_DIR, { recursive: true });
|
||||
const out = resolve(OUT_DIR, `${ID}.png`);
|
||||
await page.screenshot({ path: out, fullPage: true });
|
||||
console.log(`captured ${PEEK} -> ${out}`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import { chromium, type Page, type Locator } from 'playwright';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { DEFAULT_MASK, shots, type Shot } from './shots.config.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const OUT_DIR = resolve(__dirname, '../../src/assets/screenshots/whp');
|
||||
|
||||
function envOrDie(name: string): string {
|
||||
const v = process.env[name];
|
||||
if (!v) {
|
||||
throw new Error(
|
||||
`Missing env var: ${name}. Put it in tools/screenshots/.env (gitignored).`,
|
||||
);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
const WHP_BASE = envOrDie('WHP_BASE'); // e.g., https://whp01.cloud-hosting.io:8443
|
||||
const WHP_USER = envOrDie('WHP_USER');
|
||||
const WHP_PASS = envOrDie('WHP_PASS');
|
||||
|
||||
async function login(page: Page): Promise<void> {
|
||||
await page.goto(`${WHP_BASE}/login.php`);
|
||||
await page.fill('input[name="user"]', WHP_USER);
|
||||
await page.fill('input[name="password"]', WHP_PASS);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
/**
|
||||
* Neutralise server-identifying and account-specific text/labels before the
|
||||
* screenshot — we run a multi-server fleet, so docs shouldn't bake in a single
|
||||
* host's name or IP.
|
||||
*
|
||||
* Runs in the page context. Self-contained: no external bindings, no eval.
|
||||
*/
|
||||
async function redactSensitive(page: Page): Promise<void> {
|
||||
await page.addStyleTag({
|
||||
content: `
|
||||
.navbar-text { visibility: hidden !important; }
|
||||
.brand-full { visibility: hidden !important; }
|
||||
`,
|
||||
});
|
||||
|
||||
// String swaps for inline text that mask-by-selector can't cover.
|
||||
await page.evaluate(() => {
|
||||
const root = document.body;
|
||||
const swaps: [RegExp, string][] = [
|
||||
// server / mail / nameserver hostnames (with possible -s3 etc. suffix)
|
||||
[/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '<your-server>.cloud-hosting.io'],
|
||||
[/mail\d+\.cloud-hosting\.io/gi, '<your-mail-server>.cloud-hosting.io'],
|
||||
[/ns[12]\.whp\d+\.cloud-hosting\.io/gi, 'ns<n>.your-server.cloud-hosting.io'],
|
||||
// backup target / bucket names that bake in server number
|
||||
[/whp\d+(-[a-z0-9]+)?\b/gi, '<your-server>'],
|
||||
[/WHP\d+(-[A-Z0-9]+)?\b/g, '<YOUR-SERVER>'],
|
||||
// IP addresses
|
||||
[/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '<your-server-IP>'],
|
||||
// home dir + demo user
|
||||
[/\/docker\/users\/[a-z0-9-]+/g, '/docker/users/<your-username>'],
|
||||
[/demo-user/g, 'your-username'],
|
||||
];
|
||||
const walker = document.createTreeWalker(root, 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, replacement] of swaps) v = v.replace(re, replacement);
|
||||
if (v !== node.nodeValue) node.nodeValue = v;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function captureShot(page: Page, shot: Shot): Promise<void> {
|
||||
const viewport = shot.viewport ?? { width: 1440, height: 900 };
|
||||
await page.setViewportSize(viewport);
|
||||
|
||||
await page.goto(`${WHP_BASE}${shot.path}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
if (shot.waitFor) await page.waitForSelector(shot.waitFor);
|
||||
await redactSensitive(page);
|
||||
|
||||
const maskSelectors = [...DEFAULT_MASK, ...(shot.mask ?? [])];
|
||||
const maskLocators: Locator[] = maskSelectors.map((sel) => page.locator(sel));
|
||||
const outPath = resolve(OUT_DIR, `${shot.id}.png`);
|
||||
|
||||
if (shot.selector) {
|
||||
await page.locator(shot.selector).screenshot({
|
||||
path: outPath,
|
||||
mask: maskLocators,
|
||||
});
|
||||
} else {
|
||||
// Viewport-only — Playwright never includes browser chrome
|
||||
await page.screenshot({
|
||||
path: outPath,
|
||||
fullPage: false,
|
||||
mask: maskLocators,
|
||||
});
|
||||
}
|
||||
console.log(`captured ${shot.id} -> ${outPath}`);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
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);
|
||||
for (const shot of shots) {
|
||||
await captureShot(page, shot);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
export type Shot = {
|
||||
/** Stable filename id; output is src/assets/screenshots/whp/<id>.png */
|
||||
id: string;
|
||||
/** Path on the WHP host. Leading slash, no scheme/host. */
|
||||
path: string;
|
||||
/** Optional CSS selector to clip to a region instead of the full viewport */
|
||||
selector?: string;
|
||||
/** Optional list of selectors to redact (account ID, IPs, etc.) before snapshot */
|
||||
mask?: string[];
|
||||
/** Defaults: 1440x900 */
|
||||
viewport?: { width: number; height: number };
|
||||
/** Optional selector to wait for before capturing */
|
||||
waitFor?: string;
|
||||
};
|
||||
|
||||
/** Always-applied redactions. Per-shot mask is added on top of this list. */
|
||||
export const DEFAULT_MASK: string[] = [
|
||||
// Conservative defaults — the actual selectors may differ; verify by inspecting
|
||||
// a captured shot and adding more selectors here if anything leaks.
|
||||
];
|
||||
|
||||
export const shots: Shot[] = [
|
||||
{ id: 'whp-dashboard', path: '/index.php?page=dashboard' },
|
||||
{ id: 'whp-domains', path: '/index.php?page=domains' },
|
||||
{ id: 'whp-sites', path: '/index.php?page=sites' },
|
||||
{ id: 'whp-email', path: '/index.php?page=email-management' },
|
||||
{ id: 'whp-backups', path: '/index.php?page=backups' },
|
||||
{ id: 'whp-site-monitoring', path: '/index.php?page=ai-monitor-customer' },
|
||||
{ id: 'whp-mysql', path: '/index.php?page=database-management' },
|
||||
{ id: 'whp-postgres', path: '/index.php?page=postgresql-management' },
|
||||
{ id: 'whp-valkey', path: '/index.php?page=account-valkey' },
|
||||
];
|
||||