- Admin Docker page: add App Catalog tab (60 apps, account-picker modal)
- Admin Docker page: add dockerAdminLaunchApp() for launching apps on behalf of any account
- User panel: add 'Remove All My Apps' button — stops/removes only that user's own containers and stacks
- API: add uninstall-account action (user-scoped; admin can specify account_id, users limited to own account)
- Admin panel: no global Docker uninstall (would affect all users on the server)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- Settings page now loads current values from DB and saves via save-option API
- check-novacpx-update reads update_channel setting, checks origin/main or origin/beta
- apply-novacpx-update pulls from channel branch, fixes backup dir (/tmp), fixes SQLite migration syntax, records new version in novacpx_version table + settings.panel_version
- deploy-runner.sh reads update_channel from DB, pulls correct branch, records version after deploy
- webhook.php accepts pushes to both main and beta branches
- Updates page shows channel badge and latest remote version
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- date -u +%H:%M:%S UTC → ts() helper with date -u +"%H:%M:%S UTC"
(UTC as a separate word was being treated as an extra date argument)
- Backup dir changed from /var/novacpx/backups/ (root-owned, doesn't exist)
to /tmp/novacpx-backup-TIMESTAMP/ (always writable by www-data)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- check-novacpx-update and check-os-update return cached data (12h TTL)
immediately instead of running slow git fetch / apt-get update on page load
- Cache stored in settings table (update_cache_novacpx, update_cache_os)
- Updates page shows "Cached · last checked X ago" when serving cache
- "Refresh now" button forces a live re-check and updates cache
- bin/cache-update-check.php: standalone cron script that warms cache nightly
- Cron registered at 2am daily on panel server
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
launchFromCatalog creates compose stacks, not docker_containers entries.
Replace My Containers tab with My Apps tab backed by docker/stacks endpoint.
Add Refresh, Start/Stop, Logs, Remove actions per stack row.
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>
Dots/dashes in names were failing validateName; now stripped to underscores.
Empty db_user field sent as "" (not null) so ?? fallback never fired; fixed
to check for empty string explicitly. Wrap createMySQL/Postgres in try/catch
so validation errors return 400 JSON instead of 500. Also pass db_type from
JS (was being sent as db_type not type).
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>
Constants were only defined in api/index.php, so direct requests to
admin/reseller/user index.php got a fatal error before rendering any HTML.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- .github/workflows/version-bump.yml: auto-increment patch version on push to main/beta
- admin/index.php: show version under logo from VERSION file
- system.php: service-versions endpoint (catalog of 22 services with version/description/status)
- admin.js: updates page shows Installed Services table with current/latest/status/description
- admin.js: loadServiceVersions() lazy-loaded after page render via setTimeout
- admin/index.php: separate Fail2Ban sidebar entry (was merged into Firewall label)
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
- proxy.php: always set proxy_mode=disabled and clear remote_host/backend_ip
on any uninstall, not just when nginx binary is removed
- admin.js: show setup cards when mode==='disabled' regardless of whether
nginx binary still exists on the remote VM
- Status card shows 'Disabled' instead of 'Stopped' when mode is disabled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NOVACPX_ROOT (/srv/novacpx/public) is a deployed file copy, not a git
repo — hence 'fatal: not a git repository'. The actual git clone lives
at /opt/novacpx-src (installed by the installer).
check-update and apply-update now use /opt/novacpx-src for all git
operations. apply-update also deploys the pulled files back to the web
root with cp -a (public/, api/, lib/, bin/) and re-sets ownership.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nova.modal(title, body, footerHtml) expects an HTML string for the third
parameter. proxyAddHost, proxyEditHost, proxySwitchLocal, and proxyUninstall
were all passing async functions instead, which got stringified as garbage
text with no actual buttons rendered.
Each modal now gets proper Cancel + action button footer HTML, with the
save/action logic wired via addEventListener after modal creation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
proxyRunSetup() used EventSource which only sends GET. The setup-remote
endpoint requires POST, so the request hit the 404 default and the SSE
connection immediately errored with 'Connection lost'.
Replace EventSource with fetch+ReadableStream (same pattern already used
by proxySwitchLocal). Also remove the dead EventSource+close() pair that
was left in proxySwitchLocal from an earlier draft.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
Only apply the tight 10/min bucket to POST /auth (actual login attempts).
GET /auth (session checks on page load) now falls into the general 120/min
bucket, preventing the login page from rate-limiting itself during normal use.
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>
- 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>
JS sends account_id but PHP was reading id; both now accept either.
Same fix applied to terminate.
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>