- install.sh: replace /usr/sbin/ufw * with scoped subcommands
- install.sh: remove /usr/bin/curl * and /usr/bin/env * NOPASSWD (trivial root escalation)
- PHPManager: switchVersion() uses sudo rm -f instead of unlink() for old pool
- PHPManager: updateConfig() SQLite syntax (ON CONFLICT / datetime('now'))
- WordPressManager: cloneStaging() escapeshellarg() on all shell-interpolated paths
- WordPressManager: delete() removes DB record before filesystem to avoid phantom records
- WordPressManager: ensureWpCli() validates download size and enforces 30s timeout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs that together left stale pool files behind after termination,
crashing php-fpm on next startup (exit-code 78, user not found):
1. removePool() used file_exists() to guard the rm — fails silently when
www-data can't read /etc/php/*/fpm/pool.d/; now always attempts sudo rm -f
2. reloadFPM() called systemctl without sudo — silently failed as www-data,
leaving the old pool loaded even when the file was successfully removed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AccountManager::terminate() called DatabaseManager::drop() without
requiring the class first — fatal class not found error on every
account termination.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Core.php: add DB_HOST constant (was undefined, causing fatal error on any
WordPress manager page load in PHP 8)
- WordPressManager: make provDb lazy (only connects to MySQL when actually
needed for install/clone/delete — not on list/info which only use SQLite)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Docker app launch now runs docker compose up -d in background (nohup &)
so the API returns immediately instead of timing out during image pulls
- EmailManager syncPostfix: replace MySQL SUBSTRING_INDEX with SQLite SUBSTR/INSTR
- EmailManager syncPostfix: write postfix files via sudo tee (www-data permission fix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- domains: VhostManager::create() called with array instead of 4 params
- PHPManager: VhostManager not required; pool writes use sudo tee (permission);
updateConfig creates pool if missing instead of throwing
- DatabaseManager: MySQL ops used SQLite panel PDO; add dedicated mysqlPdo()
using MariaDB socket auth
- BackupManager: column name is size_mb not size; diskUsage returns float
- DB.php: add LAST_INSERT_ID() → last_insert_rowid() translation
- user.js: SSL issue/submit used Nova.api (JSON) but endpoint streams SSE;
add _sslStream() helper matching admin panel behavior
- schema/migration: add enc_password column to email_accounts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Panel no longer depends on the user-managed MariaDB service.
SQLite at /var/lib/novacpx/panel.db runs independently so the
control panel stays up even when MariaDB is stopped.
- DB.php: switch to sqlite: DSN, add SQL translator (ON DUPLICATE KEY,
DATE_ADD/DATE_SUB INTERVAL, NOW(), UNIX_TIMESTAMP(), IFNULL)
- Core.php: replace DB_HOST/NAME/USER/PASS with DB_PATH constant
- schema.sql: full SQLite syntax, add TOTP columns to users table
- _branding.php: use sqlite: PDO, datetime('now') for session check
- install.sh: apt install sqlite3, create SQLite DB instead of MySQL DB
- tools/migrate-to-sqlite.sh: one-shot migration script for existing installs
remoteExec uses 2>&1 so SSH's own stderr merged with command stdout.
The 'Warning: Permanently added...' line prepended to 'active' made the
=== 'active' check in isRunning() always return false.
Add -o LogLevel=ERROR to suppress SSH informational/warning messages
while keeping actual remote command output and real SSH errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ProxyManager::sysctl() and reload() now use sudo for local commands —
www-data cannot run systemctl directly, so start/stop/restart/reload
were silently failing with permission denied
- Control endpoint now returns success:false when nginx stays stopped
after a start/restart, or stays running after a stop
- proxyControl() JS shows a loading overlay while the action runs and
uses error toast when the action reports failure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add -o UserKnownHostsFile=/dev/null to remoteExec so SSH doesn't
attempt to write to /var/www/.ssh/known_hosts (permission denied).
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>
- 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>
- Notifier.php + test-notify: use plain email address in 'from' field
(CyberMail rejects "Name <email>" format)
- deploy-runner.sh: rsync panel/api/ and panel/lib/ to web root after
panel/public/ sync; also syncs panel/bin/ to /opt/novacpx/bin/
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>
- Use sudo for mkdir/chown/chmod in home dir setup so www-data can execute
- Set public_html to 775 (group-writable) so www-data can deploy index.html
- Remove duplicate SPF from createZone defaults (provisionEmailDNS owns SPF/DMARC/DKIM)
- sudo mkdir/chown in provisionEmailDNS for opendkim key directory
- 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>
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>