Major client-area overhaul, WHMCS 9 + VirtFusion v7 compatibility, and a
hardening pass on every destructive client.php endpoint.
Tested against WHMCS 9.0.3 + VirtFusion v7.0.0 Build 9.
Features
- "On This Page" jump-link group injected into the WHMCS Actions sidebar
via ClientAreaPrimarySidebar; auto-hides links for hidden panels.
- Monthly traffic chart (last 12 months) with rx/tx bars and centered
legend; replaces the dead canvas that read non-existent JSON paths.
- Live Stats panel: CPU, memory, disk I/O from remoteState; 30s refresh
while the panel is visible AND the page has focus.
- Filesystem usage rows in the Resources panel from qemu-guest-agent
fsinfo; pseudo-FS filtered out.
- Server Overview meta chips: data-center location with country flag,
OS template/agent name with kernel on hover, "Created N days ago".
- Hypervisor maintenance banner at the top of the page.
- Mask Sensitive screenshot mode: IPv4 keeps first two octets, IPv6
keeps first two hextets, hostnames keep first char per dot-label.
Inputs masked via text-security: disc; covers Server Name + Hostname
+ IP cells + rDNS panel rows.
- Per-IP copy buttons folded into the Server Overview cells (replaces
the deleted standalone Network panel).
- VNC viewer popup served from a same-origin authenticated route
(client.php?action=vncViewer) — POST + requireSameOrigin, rotates
the wss token on every open, X-Frame-Options DENY, strict CSP.
Bug Fixes
- UsageUpdate cron silently no-op'd: read server.usage.traffic.used
which doesn't exist. Bandwidth now from /servers/{id}/traffic;
disk usage from remoteState.agent.fsinfo.
- WHMCS 9 multi-service order short-circuit: AfterModuleCreate's
AcceptOrder fired after the first service and terminated the batch
loop, orphaning siblings. Defer until every VF service in the order
has a server_id.
- Orphaned services produced six generic 500s; new
requireProvisionedService() helper emits one clean 409 with an
actionable message. Wired into all 17 client.php cases.
- Server Overview Traffic showed "- / Unlimited"; now renders real
bytes and "Unmetered" (limit=0 is per-period uncapped, not feature-off).
- Rename endpoint moved to PUT /servers/{id}/modify/name in VF v7
(was 404'ing); response is HTTP 201 not 200/204.
- Rename was force-lowercasing the input; relaxed validation to
preserve case + freeze the input row mid-flight to prevent
double-submits.
- "Other" OS category icon override removed; uses VirtFusion's icon
instead of a hardcoded SVG.
- Save button squish on the rename row fixed via flex-wrap layout.
Security
- CSRF protection (requirePost + requireSameOrigin) added to every
destructive POST: rebuild, resetPassword, resetServerPassword,
powerAction, rename, selfServiceAddCredit, toggleVnc, vncViewer.
Previously only rdnsUpdate had it.
- Open-redirect defence in Module::fetchLoginTokens — refuses to
return a redirect URL whose host doesn't match the configured VF
panel hostname.
- Per-action rate limiting via new Module::requireRateLimit helper
(Cache-backed): rebuild 60s, resetPassword/resetServerPassword 30s,
powerAction 10s, vncViewer/toggleVnc/selfServiceAddCredit 5s.
- vncViewer route delivers strict Content-Security-Policy
(default-src none, script-src self + VF panel, connect-src wss VF
panel, frame-ancestors none).
- IPv6 examples in placeholder/comments switched to the IANA
documentation prefix 2001:db8::/32 (RFC 3849).
Removed
- Network panel (duplicated Server Overview IP rows).
- VNC enable/disable toggle (VF firewall flag is non-functional;
toggle was misleading).
- Network Speed row in Resources panel (always 0 from VF API).
Internal
- Module::fetchServerData now passes ?remoteState=true.
- ServerResource::process exposes osName/osPretty/osKernel/osDistro/
osIcon/location/locationIcon/hypervisorMaintenance/createdAt/
builtAt/live.* fields.
- Module::toggleVnc corrected to send {vnc:bool} (the actual API
param) instead of {enabled:bool} (silent no-op).
- Module::getVncConsole + toggleVnc return baseUrl alongside the
envelope so the viewer route can build the wss URL.
- Panel margins tightened mb-3 → mb-2 across all 11 panels.
The stock-control safety bullets and algorithm description called
`primaryStorageProfile` a "profile id" matched against `otherStorage[].id`.
That mirrored the buggy implementation rather than the actual VirtFusion
contract: it's a storage *type code* (mirrors `server_packages.storage_type`)
that filters against `otherStorage[].storageType`. Updated CLAUDE.md and
README.md so future readers don't repeat the bug. Also documents the new
behavior of walking all matching pools and picking the largest fit, with
disabled peers skipped rather than treated as fatal.
The `storageProfile` mapping table row is intentionally left untouched —
that documents an admin-facing configurable-option alias, and renaming it
could quietly invalidate existing operator setups.
Opt-in per product via WHMCS's native tblproducts.stockcontrol toggle.
When enabled, the module overwrites tblproducts.qty with the number of
VPSes the panel can still actually provision, derived from two
authoritative sources:
- GET /packages/{id} for the per-VPS resource footprint (memory,
cpuCores, primaryStorage, primaryStorageProfile, enabled)
- GET /compute/hypervisors/groups/{id}/resources for live
free/allocated data per hypervisor in the group
Algorithm sums min(memory, cpu, storage) across eligible hypervisors
(enabled AND commissioned AND !prohibit) for every group the product
can be placed in (default configoption1 plus every numeric value of a
Location configurable option), capped by the group-level IPv4 pool
taken as max() within a group to avoid double-counting. Storage
matching is strict against package.primaryStorageProfile; hypervisors
without the named pool contribute 0.
FAIL-SAFE INVARIANT: transient API failures return null from
Module::fetchPackage / Module::fetchGroupResources, and the orchestrator
leaves tblproducts.qty UNCHANGED in that case. Confirmed-missing
conditions (HTTP 404, package.enabled=false) return qty=0. Without this
tri-state contract the module would either zero out inventory during
API blips, or show inventory for packages that have been deleted.
Triggers:
- AfterModuleCreate: refresh + auto-accept pending order
- AfterModuleTerminate: refresh (capacity came back)
- AfterCronJob: every-2-hour safety net for out-of-band panel changes
- ClientAreaPageCart: opportunistic per-product refresh in order flow
- admin.php?action=stockRecalculate: on-demand full recalc
Shared 30s rate-limit (stockrefresh:event) coalesces provision bursts;
60s per-product limit (stockrefresh:{pid}) caps cart-page refreshes;
grpres:{id} 120s TTL caps upstream API reads per group regardless of
how often hooks fire.
Auto-accept: AfterModuleCreate calls WHMCS AcceptOrder with
autosetup=false when the parent order is still Pending. Idempotent;
already-accepted orders are skipped via strcasecmp status check.
New per-product config option stockSafetyBufferPct (configoption7,
default 10) reserves X% of each resource's max before computing fits.
Blank falls back to 10% so existing products get headroom without any
config change. Ignored for unlimited resources (max=0) and for IPv4
(no per-hypervisor max in the response).
TestConnection now probes /compute/hypervisors/groups to surface
missing compute:read scope at config time instead of as unexplained
nightly silence.
Introduces an opt-in reverse DNS management subsystem backed by a PowerDNS
Authoritative HTTP API. Runs via a companion WHMCS addon module
(modules/addons/VirtFusionDns) that holds settings and a Test Connection
page; the server module reads those settings from tbladdonmodules and
short-circuits when the addon is absent or disabled, so provisioning is
unaffected for operators who don't use the feature.
Lifecycle hooks:
- createAccount creates PTRs for every assigned IP (forward DNS must
already resolve to the IP — FCrDNS enforcement)
- renameServer updates only PTRs whose content matched the old hostname,
preserving client-custom records
- terminateAccount deletes all PTRs before the local state is purged
- TestConnection merges PowerDNS health check with the existing VirtFusion
check
- A DailyCronJob hook reconciles missing PTRs additive-only (never
overwrites)
Client UI: new "Reverse DNS" panel on the service overview with one
editable PTR input per assigned IP, per-row status badges, and
forward-DNS rejection on save. Admin services tab gets a parallel
widget with Reconcile (additive) and Reconcile (force reset) buttons.
New subsystem at lib/PowerDns/:
- Client.php PowerDNS API wrapper (X-API-Key, listZones/getZone/
patchRRset/notifyZone), auto-NOTIFY on successful PATCH
- Config.php Loads + decrypts addon settings from tbladdonmodules
- IpUtil.php PTR-name generation (IPv4 + IPv6), zone matching,
RFC 2317 classless parsing
- Resolver.php FCrDNS verification via dns_get_record with CNAME-chain
following and per-(hostname,ip) caching
- PtrManager.php Orchestrator: syncServer, deleteForServer, listPtrs,
setPtr, reconcile, reconcileAll
Security hardening helpers added to Module and applied to the rDNS
endpoints:
- requirePost() HTTP method gate (405 on non-POST mutations)
- requireSameOrigin() Origin/Referer check against WHMCS host (CSRF
defence against cross-site form POST)
- requireServiceStatus() tblhosting.domainstatus filter (Active for
writes, Active+Suspended for reads)
RFC 2317 classless delegations (e.g. 64/64.113.0.203.in-addr.arpa.)
supported with alignment validation: rejects misaligned start addresses
that don't correspond to any real delegation boundary.
PowerDNS zone IDs containing '/' are URL-encoded as '=2F' per the
PowerDNS API convention. PATCH success triggers PUT /zones/{id}/notify
so slaves pick up the SOA-bumped serial immediately.
Includes IPv4 + IPv6 support, per-IP write rate limit (10s), fresh
IP-ownership re-verification on every client write (defends against
stale-ownership after IP reassignment), and audit logging of every
successful edit to the WHMCS module log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Auto-create 'Initial Operating System' and 'Initial SSH Key' custom fields
via Database::ensureCustomFields() on module load, eliminating the manual
modify.sql step
- Delete modify.sql (no longer needed)
- Add try/catch blocks around every DB operation and API call across all PHP
files per CLAUDE.md error handling rules
- Add comprehensive PHPDoc to all classes, methods, and properties
- Set up Laravel Pint (laravel/pint) with Laravel-style preset for consistent
code formatting across the codebase
- Add git pre-commit hook (hooks/pre-commit) that runs Pint on staged PHP
files, auto-installed via Composer post-install/post-update scripts
- Simplify README installation to a single copy-paste command
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Escape $serverObject and $systemUrl in AdminHTML.php heredocs to prevent XSS
- Add null guard in Database::getSystemUrl() to prevent fatal error
- Guard primaryNetwork access in module.js to prevent null dereference
- Reset badge/traffic-bar CSS classes on refresh to prevent accumulation
- Add VNC popup-blocked check with user-facing message
- Add BS3 input-group-btn dual class for theme compatibility
- Escape billing template variables with |escape:'htmlall'
- Add cache-busting to admin CSS/JS includes
- Switch cache-busting format from version to date-based (20260207)
- Create .releaserc.json for automated CHANGELOG.md management
- Add changelog/git plugins to semantic-release workflow
- Remove manual [Unreleased] section from CHANGELOG.md
- Update README: install/upgrade with rsync, accuracy fixes, add keygen.js
- Update CLAUDE.md: add keygen.js, document removed features
- Fix SECURITY.md grammar and version operator
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- VNC panel auto-hides when VNC is disabled on the server
- SSH key paste textarea at checkout with API key creation during provisioning
- Resources panel with current allocation, traffic progress bar, and upgrade link
- changePackage() now applies individual resource modifications from configurable options
- Order form configurable option dropdowns replaced with styled range sliders
- Self-service billing: credit balance, usage breakdown, credit top-up from client area
- Self-service config options (mode, auto top-off threshold/amount) on products
- Auto top-off via WHMCS cron when credit falls below threshold
- CHANGELOG.md covering all versions from 0.0.6 to present
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>