- New accounts/update endpoint: updates package_id, php_version, email,
and notes; switches PHP-FPM pool when version changes
- Edit button on each account row opens pre-populated modal
- Modal shows email, package dropdown, PHP version selector; domain
is read-only with tooltip explaining it can't change
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix accounts list always showing empty: Response::paginate() returns data
as res.data (array), not res.data.accounts — fix all 9 call sites in admin.js
- Replace blocking apply-os-update with background job + terminal modal:
start-os-update runs apt-get as nohup subprocess with sudo, writes to
/tmp log file; os-update-status polls log and done-file; admin.js shows
scrolling terminal modal that auto-closes when complete
- Fix OS update: was running apt-get without sudo (www-data lacks root)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- save-option: inline service switching (web/ftp/dns) instead of missing shell scripts
- stats: dynamic service list based on web_server/ftp_server/dns_server settings
- service action: allow all variants (nginx, pure-ftpd, pdns, nsd, etc.)
- mysqlManager: full rewrite with MySQL/MariaDB/PostgreSQL engine cards (install/remove/start/stop), active engine selector, all-databases table
- ftpServer page: dynamic — shows whichever FTP server is configured, not hardcoded proftpd
- db-engine-action: fixed duplicate INSERT line
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
php.php: install-version, remove-version, version-extensions, install-extension,
remove-extension, fpm-action endpoints. versions now returns fpm_active status
and panel_php (current runtime version).
admin.js phpManager(): grid of installed/not-installed versions with Install/
Remove/Restart FPM buttons; Extensions panel slides in below with filterable
list, dropdown of common extensions + custom input, per-extension Remove buttons.
Panel PHP info card shows which version NovaCPX runs on.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use btn-xs + flex-wrap so Restart/Start/Stop fit within the stat-card
on narrow layouts without bleeding into adjacent cards.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Stubs inside the IIFE (twofa/sessions/cloudflare/wordpress/nginxProxy)
always shadowed the real global implementations. Pages dict captured
the stubs so clicking those nav items just showed "Loading...".
Fix: stubs now delegate to renamed global functions (*Page suffix).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
system.php: INSERT used wrong column names (cpu_pct/ram_pct/disk_pct/
load_1m) — table has cpu_usage/ram_usage/disk_usage/load_avg matching
migration 007. Fatal PDOException was crashing stats API → all services
showing as unknown.
admin.js mailServer(): remove spamassassin row (not installed, was
permanently hardcoded to 'unknown').
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extra } after auditLog helpers was closing the outer async IIFE early,
causing phpManager() and all subsequent functions to parse outside the
IIFE — resulting in "Unexpected token async" crash and a blank panel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Notifier.php: CyberMail API sender with 4 trigger types (account
created, suspended, disk quota warning, SSL expiry)
- Reads cybermail_api_key / notify_from_* / notify_admin_email from
settings table
- accounts.php: fires Notifier on create (welcome + admin alert) and
suspend (user + admin alert)
- system.php: notify-settings GET, save-notify-settings POST,
test-notify POST (with API key masking)
- bin/notify-checks.php: daily cron for disk ≥85% and SSL ≤14 days
(flag-based dedup in settings table)
- admin panel: Notifications page with form + trigger reference table;
sidebar link added
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#18: reseller_branding table (migration 008). branding.php endpoint: get/save/
upload-logo/delete-logo/resellers. _branding.php server-side helper injects
CSS vars (--primary, --accent), custom CSS, favicon, and panel name into
<head> of reseller + user portals at page-load time (no flash of unbranded
content). NOVACPX_BRANDING JS global carries panel_name/support_email/
support_url/hide_powered_by for runtime use. Reseller panel gets a new
"White Label" sidebar page with logo upload, color pickers with live preview,
support contact fields, powered-by toggle, and custom CSS textarea.
#24: audit-log backend now accepts user/action/date_from/date_to filter params.
auditLog() JS rebuilt: filter bar at top, paginated table, expandable detail
rows (click row to show JSON detail), total entry count, page buttons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#26 Mobile responsive:
- Hamburger button (SVG) in topbar for all three panels (admin/user/reseller)
- Sidebar overlay div for click-outside-to-close on mobile
- nova.js: DOMContentLoaded toggle handler with overlay and auto-close on nav click
- nova.css: sidebar-overlay, page-header, panel/panel-header, table, btn-success/warning/danger/secondary/xs,
badge-muted; mobile media query shows toggle, fixes stats-grid/modal/panel-header layout
#27 Custom error pages:
- /errors/404.php and /errors/500.php with NovaCPX dark theme matching panel design
- Apache ErrorDocument 400/401/403/404/500/503 for ports 8880/8881/8882 with Alias /errors
#28 API rate limiting:
- api_rate_limits table (migration 004) with per-IP per-bucket counters
- api/index.php: 10 req/min for auth endpoint, 120 req/min for all others
- Returns X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset headers
- Returns 429 Too Many Requests when exceeded; rate limit failure is non-fatal
#29 Session Manager:
- sessions.php endpoint: list/revoke/revoke-user/revoke-all
- Admin panel Sessions page: table of active sessions with user, role, IP, browser, timestamps
- Revoke single session, revoke all for user, revoke all sessions (self-evicts)
- firewall.php: auto-detect server IPs (loopback, all interface IPs,
private /24 subnets) for Fail2Ban ignoreip; f2b-ignoreip-list/add/
remove/reset actions; write to jail.local directly (www-data owns it);
f2b_set_ignoreip() reloads fail2ban after every change
- auth.php: log failed logins to /var/log/novacpx/access.log in format
fail2ban filters expect — "FAILED LOGIN from <IP> [portal]"
- deploy/fail2ban/: filter.d conf files for all 4 NovaCPX jails
- install.sh: auto-detect local IPs → ignoreip in jail.local; install
filter files; create access.log (www-data:www-data 664)
- admin.js: Fail2Ban Whitelist section in firewall page — chip list with
add/remove/reset; loopback shown with lock icon and non-removable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New firewall.php endpoint: status, enable/disable, add-rule (full UFW
syntax: action/direction/port/proto/from/to/comment), delete-rule by
number, quick allow-port/deny-port, allow-ip/block-ip with DB storage,
ip-lists, reset to defaults, default-policy, set-logging, f2b-status
(all jails with banned counts), f2b-jail detail, f2b-ban, f2b-unban
(single jail or all), f2b-reload, f2b-restart, raw ufw command (whitelisted)
- admin.js: full firewall page — UFW status badge + enable/disable toggle,
default policy dropdowns, numbered rules table with delete, quick rule
inline form, full add-rule modal, trusted IP chip list, blocked IP chip
list, Fail2Ban jails table with banned counts, per-jail banned IP modal
with individual unban buttons, manual ban modal, logging level control
- nova.js: add Nova.escHtml() used across all new pages
- admin.js: remove git_remote field from admin settings panel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AccountManager: auto-generate DKIM keypair + inject SPF/DKIM/DMARC DNS records on account create
- AccountManager: rotateDKIM() method for key rotation with new selector
- New dkim.php endpoint: list/view/rotate/provision DKIM keys per domain
- schema.sql: add dkim_keys table
- install.sh: install opendkim, wire into Postfix milter, fix dotfile copy (. vs *), fix config.ini permissions (root:www-data 640), copy VERSION to web root, add opendkim to service restart
- api/index.php: fix NOVACPX_ROOT path (was 2 levels too high), fix CORS ports (8880-8883), VERSION fallback to /opt/novacpx-src
- api/.htaccess: route all /api/* requests through index.php
- system.php: check-os-update, apply-os-update (self-healing: auto-restart downed services, restore web root if panel ports go down), check-novacpx-update, apply-novacpx-update (PHP syntax validation before deploy, backup + restore on failure)
- admin.js: Updates page now shows both NovaCPX panel updates and OS package upgrades in one section; sidebar badge shows combined count
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Accounts: list with search/filter, suspend, unsuspend, terminate, change password
Resellers: list, create reseller modal
Packages: full CRUD with all limit fields
DNS Zones: list all zones, add/delete zones, add/delete records
Nameservers: hostname + NS1/NS2 configuration via server_setup API
Web Server: service control with restart/start/stop per service
SSL Manager: all certs table, bulk issue for all domains, renew, delete
Firewall: UFW allow/block ports, Fail2Ban unban, jail status
MySQL Manager: all databases with drop
Mail Server: Postfix/Dovecot service control, mail queue viewer
FTP Server: ProFTPD service management
Backups: per-account backup now + backup all
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each panel now has its own dedicated port and is fully self-contained:
- Port 8880: User panel (end-user hosting dashboard)
- Port 8881: Reseller panel (account/package management)
- Port 8882: Admin panel (datacenter/server manager)
Changes:
- install.sh: PORT_USER/PORT_RESELLER/PORT_ADMIN constants; three separate
nginx/Apache vhosts; UFW opens all three ports; Fail2Ban jail per port;
credentials file shows all three URLs
- config.ini: stores port_user/port_reseller/port_admin
- Core.php: defines PORT_USER/RESELLER/ADMIN, detects CURRENT_PORTAL from
SERVER_PORT so the API knows which tier is being accessed
- Auth.php: portalUrl() maps role → correct port for cross-portal redirects
- auth.php endpoint: returns portal_url on login so JS redirects to right port
- index.php login: uses portal_url from API response (no hardcoded paths)
- admin/index.php: inline login form (port 8882 is self-contained, no redirect)
- user/index.php: inline login form (port 8880 self-contained)
- reseller/index.php: new full reseller panel with inline login (port 8881);
sidebar with accounts, packages, DNS, branding, bandwidth report sections
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>