1. admin.js: dashboard setTimeout was after return (dead code) — restructured
to assign template to const html, run setTimeout, then return html
2. DockerManager.php createStack: replaced SELECT LAST_INSERT_ID() with
db->insert() which already returns lastInsertId correctly for SQLite
3. DockerManager.php setQuota: replaced ON DUPLICATE KEY UPDATE / VALUES()
MySQL syntax with SQLite-compatible ON CONFLICT(user_id) DO UPDATE SET
excluded.col syntax
4. post-restore.sh: PHP helper file now written ONCE at start of step 4
before any call to it (was written AFTER first call, causing silent failure)
5. post-restore.sh: git pull exit code now captured before pipeline (the
while-read loop always exited 0, masking pull failures)
6. uninstall.sh: tar backup now aborts on failure (previously 2>/dev/null
swallowed errors and rm -rf destroyed source unconditionally); also
rm -f → rm -rf for .service.d drop-in directory
- Added history chart to dashboard: fetches stats/server API, renders
24-hour CPU/RAM/Disk chart with Chart.js lazy-loaded
- setTimeout properly INSIDE function before closing brace
- Removed ONLY serverStatus() function body (2521 chars), kept initStatsChart
- pages object redirects server-status → dashboard
- Removed server-status from admin sidebar nav
- All 26 functions intact, backticks balanced, accounts/packages/DNS all kept
- Adminer installed at /adminer.php (MySQL + PostgreSQL)
- db-tools API now detects adminer.php file and returns its URL
- Tool cards: phpMyAdmin, Adminer (MySQL/PG), pgAdmin4
- Open buttons use API-provided URL (adminer.php for Adminer)
- Separate MySQL and PostgreSQL database sections in DB Manager
- PostgreSQL section has direct link to Adminer PG mode
- #42 Docker: already complete (full docker page with all tabs)
Previous merge accidentally deleted 38KB of page functions (accounts, packages,
DNS, etc.) by using wrong boundary. This time only removes the serverStatus()
function body. Dashboard now includes history chart + setTimeout to render it.
All other pages intact.
- Row badge updates to 'stopping…'/'starting…' instantly on click
- Buttons disabled while action runs so no double-clicks
- List stays visible while refreshing after action (no blank flash)
- container-remove changed to POST so body passes through proxies correctly
- removeImage now throws RuntimeException when docker rmi output contains
'Error' or 'conflict' so the API returns success:false with the message
- Added docker/sync-orphans endpoint (admin only) to register existing
Docker containers not tracked in the NovaCPX DB (e.g. after a restore)
Concurrent cron writes (collect-stats.php every 5min) caused DB lock errors
that aborted the entire stats response, leaving web/mail/FTP pages empty.
History insert is now non-fatal.
Using PORT_USER ?? 8880 threw Error in PHP 8 since the constant isn't defined
until Core.php is require_once'd later in the file. Every API request was
hitting the exception handler and returning 'An internal error occurred.',
breaking all logins and API calls.
#38 — User panel Account > Settings page: account info, resource usage
gauges, PHP config (version/memory/upload/exec), quick links to SSL/2FA/password.
#39 — AccountManager: dark-themed modern default index.html on account
creation; supports custom HTML template from admin Server Options
(saved as default_index_template setting, {domain}/{username} placeholders).
Admin Server Options: new card to set/reset the custom template.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
Admin: global view of all subdomains/parked across accounts; nav items added
Reseller: filtered view scoped to their customers' accounts
User: create/remove subdomains and parked domains for own account
Backend already existed in api/endpoints/domains.php (add-subdomain,
add-alias, list, remove actions).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
- CORS: replace open regex with explicit hostname allowlist + port whitelist
- Exception handler: only expose RuntimeException/InvalidArgumentException
messages; PDOException and others return generic 'internal error'
- Auth::portalUrl(): allowlist-validate HTTP_HOST before using it in
redirect URL — prevents open redirect via Host header injection
- _branding.php custom_css: strip HTML tags, js: URLs, @import, expression()
instead of just </style> which was trivially bypassable
- accounts create: check accounts table as well as users for username
uniqueness (TOCTOU fix); wrap user INSERT + provisioning in single
transaction so rollback is atomic on failure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
- accounts.php: remove outer beginTransaction() — AccountManager already wraps in its own transaction; nested transactions fail in SQLite with 'already an active transaction'
- accounts.php: on AccountManager failure, manually delete the inserted user row instead
- admin/reseller/user index.php: fix favicon href from /assets/img/favicon.svg to nova-favicon.svg
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
- admin.js: 1292 lines of features were on server but not in repo — recovered and committed
- admin.js: impersonation redirect now uses location.origin instead of hardcoded :8880 port
- accounts.php: pre-validate email uniqueness and username before INSERT to prevent SQLSTATE 23000
- accounts.php: wrap user INSERT + AccountManager::create() in single transaction for full rollback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
When HTTP_HOST has no port (NPM on 443), return URL without appending panel port.
Direct access (HTTP_HOST includes :8882 etc.) still redirects to correct port.
Prevents browser being sent to :8882 directly after login via novacpx.orbishosting.com.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
- set_exception_handler in api/index.php prevents uncaught exceptions from crashing PHP-FPM
- AccountManager::create() wrapped in DB transaction with rollback + Linux user cleanup on failure
- CORS origin regex updated to allow requests from port 443 (NPM reverse proxy)
- index.html written via sudo tee instead of file_put_contents (www-data permission fix)
- chpasswd now called with sudo prefix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
- admin.js was calling auth/register (action does not exist) — changed
to users/create
- Reseller list was fetching from accounts/list which is for hosting
accounts; fixed to users/list?role=reseller
- Replaced shared adminSuspend/adminChangePass (account-scoped) with
dedicated adminResellerSuspend/Unsuspend/Passwd/Delete functions that
operate on the users table
- Added users endpoint actions: create, suspend, unsuspend,
change-password, delete — all admin-only, operating on user rows
rather than hosting account rows
- Reseller delete disowns their accounts (sets reseller_id=NULL) rather
than cascading delete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- API: stack-action/stack-remove now verify ownership for non-admin users
- API: add stack-reinstall action (pull latest images → down → up)
- User panel: add Reinstall button per stack; fix bug where remove-stack was called instead of stack-remove
- Admin panel: add Reinstall button per stack + dockerStackReinstall() handler
- User panel: Remove All My Apps now only removes the calling user's own containers/stacks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>