Nova.modal(title, bodyHtml, footerHtml) expects footerHtml as an HTML
string, but an async callback was passed instead. The function got
stringified as garbage text in the footer with no save button, so
nothing ever saved regardless of mode chosen.
Replaced with proper footer HTML (Cancel + Save buttons) and wired the
save logic as an event listener on the save button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- admin.js was calling 'apply-novacpx-update' but endpoint is 'apply-update'
- Add null guard in nova.js progress bar setInterval — _barEl can be set to
null by the fade-out timer between interval ticks, causing a crash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The redirect on 401 sent the browser back to /?redirect=... on the same
panel port, which re-served the panel page itself — causing an infinite
reload. The panel pages (admin/reseller/user) already have inline login
forms that activate when auth/me fails, so no redirect is needed.
Return a failure object on 401 so the existing login form shows instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ProxyManager::runSetupOnRemote() — generator yields step-by-step
progress; drives SSE stream from /api/proxy/setup-remote POST
- ProxyManager::uninstall(bool) — removes configs from remote or local;
optionally apt-get removes nginx and sets mode=disabled
- ProxyManager::healthCheck() — called every 5 min from collect-stats.php;
restarts nginx on remote if found stopped
- proxy.php: POST /api/proxy/setup-remote (SSE stream), DELETE /api/proxy/uninstall
- admin.js: proxyRunSetup() streams output to a live log modal;
proxyUninstall() with configs-only vs full removal choice;
'Run Setup on Remote VM' / 'Uninstall' buttons in page header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ProxyManager: all ops (start/stop/reload, config push) work over SSH
when proxy_mode=remote; sysctl/reload/writeHostConfig/deleteHost all
route to remoteExec/remotePush helpers
- proxy.php: add GET/POST /api/proxy/settings and POST /api/proxy/test-remote
- admin.js: Settings modal with mode selector + remote fields + Test Connection;
page header always shows Settings button; status card shows mode + remote host;
'not installed' state directs to Configure Remote Proxy VM
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- auth: impersonate stores empty data instead of raw cookie; unimpersonate
issues a fresh session rather than replaying a stored token
- api/index.php: restore rate limiting (10 req/min auth, 120 general)
- nova.js: 401 redirects to login instead of silently returning error;
escHtml now escapes single quotes to prevent onclick XSS
- accounts: wrap ownership-change 4-write path in beginTransaction/commit;
restore audit body on account.update
- reseller/user login cards: use $_pname instead of hardcoded 'NovaCPX'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Enforce portal role isolation: admin/reseller/user can only auth on their own port
- Admin/reseller impersonation: Login As with cookie handoff + Return banner in user panel
- Account ownership: admin can reassign accounts to resellers, DNS NS follows
- accounts/update: ownership change cascades package + NS to new owner
- users.php endpoint: admin list/filter by role (reseller dropdown)
- Docker launch fix: uDockerUpdateParams defined before call
- Nova.loading() spinners: login, SSL, PHP switch/save, backup create, docker launch/actions
- Logo consistency: gradient CPX text on all login pages, novacpx_logo_html() in all sidebars
- BackupManager: fix DB class name, table name, column name
- DNSManager: fix settings keys (ns1_hostname/ns2_hostname)
- docker.php: resolve account_id from user uid for all actions
- Auth: impersonate sets cookie + stores return_token for seamless round-trip
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- initUser/initReseller: show main-layout and hide auth-check on
successful auth (was never shown, causing blank page after login)
- Fix login form IDs in user/index.php and reseller/index.php:
changed l-user/l-pass/login-err to li-user/li-pass/li-err to
match what doLogin() reads; add onsubmit handler so form works
immediately without waiting for JS to replace it
- Wire logout button in both boot sequences
- Fix data.accounts → data in reseller.js (same paginate() bug as admin)
- Reset myron user password to Joker1974!!!
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- initUser() now hides auth-check and shows main-layout on success
- Remove conflicting inline script from user/index.php that referenced
#app (non-existent) instead of #main-layout, causing null JS error
that prevented the panel from ever rendering after successful auth
- Wire logout button in boot sequence (was only in the removed inline script)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>