129 Commits

Author SHA1 Message Date
myron 33c86171d6 fix: add null-coalescing defaults for all package update fields to suppress undefined key warnings 2026-06-23 16:07:47 +00:00
myron 3a1746b0c0 fix: all 6 code review findings
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
2026-06-23 03:13:42 +00:00
myron d24ea40505 feat: merge Dashboard + Server Status (careful surgical approach)
- 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
2026-06-23 01:53:35 +00:00
myron a98f08e45a fix: add Adminer install/remove handler to db-tools-stream; fix sudo tty for phpMyAdmin 2026-06-22 23:58:59 +00:00
myron 568e0a0891 feat: #41 #43 — phpMyAdmin + Adminer + PostgreSQL in DB Manager
- 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)
2026-06-22 21:29:12 +00:00
myron e209df0dc2 revert: restore admin.js to last known-good state (01b0995) 2026-06-22 21:19:54 +00:00
myron 42055ccdcc fix: dashboard setTimeout inside function, history chart properly merged 2026-06-22 21:13:28 +00:00
myron 3ca3a1dae6 fix: surgical dashboard merge — only remove serverStatus(), keep all other pages
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.
2026-06-22 19:13:07 +00:00
myron d39559a058 feat: merge Dashboard + Server Status into one page (admin panel) 2026-06-22 19:03:41 +00:00
myron 844f571231 fix: docker catalog CHECK constraint — change status='starting' to 'pending'
'starting' is not in the CHECK constraint list. Using 'pending' which is
already set by the INSERT in createStack (the UPDATE was redundant anyway).
2026-06-22 14:24:12 +00:00
myron 960a29f508 fix: image-remove use POST not DELETE (body was stripped by proxy); keep list visible on refresh 2026-06-22 12:51:49 +00:00
myron 6aa96e6265 fix: docker container actions update row immediately (optimistic UI), keep list visible during reload
- 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
2026-06-22 12:44:45 +00:00
myron 1c4a06d31e fix: docker image-remove throws on daemon error; add sync-orphans endpoint
- 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)
2026-06-22 12:32:45 +00:00
myron b00cf10120 fix: escape apostrophe in FTP empty-state string — caused SyntaxError in template literal 2026-06-22 12:24:07 +00:00
myron 76726dc47c feat: #41-#47 admin root controls — enhanced pages + new APIs
#41 phpMyAdmin: quick-access links in database manager
#43 PostgreSQL: Adminer at /adminer.php (MySQL + PostgreSQL)
#44 Mail server: virtual domains list, mail log tail, better service controls
#45 FTP server: full account list from DB, better service controls
#47 Web server: stats cards, PHP defaults, log viewer

New APIs: system/read-log, email/domains
Fix: PHP-FPM pm.max_children increased to 20 (was 5, causing exhaustion)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
2026-06-22 12:20:53 +00:00
myron 6b59730bec fix: user account settings page — use stats/account instead of forbidden accounts/usage+me 2026-06-22 05:11:00 +00:00
myron e0447bc5f5 sync: admin/index.php sidebar cleanup (subdomains/parked nav onclick attrs removed) 2026-06-22 05:02:20 +00:00
myron 697763f333 fix: wrap server_stats INSERT in try/catch — SQLite lock was killing stats API
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.
2026-06-22 04:52:08 +00:00
myron 9caaa65b31 fix: broken adminSubdomains/adminParked JS from bad patch; CORS PORT_* constants 2026-06-22 04:33:32 +00:00
myron 2ecf93a344 fix: hardcode panel ports in CORS check — PORT_USER etc undefined before Core.php loads
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.
2026-06-22 04:29:15 +00:00
myron 6f494e96fd feat: #38 account settings page (user panel); #39 better default index template
#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
2026-06-22 04:21:58 +00:00
myron 5d1d47a007 feat: #36 subdomains + #37 parked domains sections in all 3 panels
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
2026-06-22 04:12:41 +00:00
myron 7b11439f9c feat: #48 collapsible nav in all 3 panels; #50 post-restore automation script
- nova.js: _initCollapsibleNav() exposed as window._initCollapsibleNav
- user.js + reseller.js: call _initCollapsibleNav after renderNav()
- deploy/novacpx-post-restore.sh: fixes config.ini, pools, vhost, dashboard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
2026-06-22 04:07:00 +00:00
myron 956defc34b fix: all code review security findings
- 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
2026-06-21 16:03:26 +00:00
myron 1a907d18b0 feat: collapsible sidebar nav with localStorage state (#48)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
2026-06-20 21:17:08 +00:00
myron 65a8690750 fix: use /bin/rm explicitly in removePool so sudoers path matches
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
2026-06-20 21:04:17 +00:00
myron 91d0e625c4 fix: decouple php-fpm reload from HTTP request using flag file + cron
Reload during account creation was causing 502 by killing the fpm worker
before nginx finished reading the response. Flag file picked up by cron
within 60s instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
2026-06-20 17:06:52 +00:00
myron 9aa67f7efd fix: email uniqueness check only applies to hosting accounts, not admin/reseller users
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
2026-06-20 16:42:16 +00:00
myron eb84504689 fix: remove php-fpm pool on account creation rollback to prevent fpm crash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
2026-06-20 16:39:48 +00:00
myron 8e623427e3 fix: reload php-fpm async to prevent killing the account-creation request
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
2026-06-20 16:34:59 +00:00
myron 3ad7ee44c2 fix: nova.js 401 handler in correct panel/public path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
2026-06-20 16:09:33 +00:00
myron 3dab4ffe0f fix: show real error message on login 401, not misleading 'Session expired'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
2026-06-20 15:59:34 +00:00
myron b534e7e306 fix: nested transaction crash and favicon 404
- 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
2026-06-20 05:46:32 +00:00
myron 6dd2e3a08d fix: add all server-only assets and panel files missing from repo
Previously missing from git (rsync --delete was wiping them on every deploy):
- assets/css/nova.css
- assets/js/nova.js, features.js, reseller.js, user.js
- assets/img/*.svg (favicon, icons, logo, mark)
- index.php, _branding.php, errors/404.php, errors/500.php
- reseller/index.php, user/index.php

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP9Q4kfCAYAjJnsbHBrViZ
2026-06-20 05:40:00 +00:00
myron b077226581 fix: recover full admin.js from server, fix port redirects and account create validation
- 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
2026-06-20 05:33:35 +00:00
myron 91bf8d965f fix: portalUrl stays on proxy host when accessed via reverse proxy on port 443
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
2026-06-20 05:27:34 +00:00
myron 39942929a7 fix: global exception handler (prevents 502), transaction rollback on account create, CORS for reverse proxy
- 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
2026-06-20 05:23:42 +00:00
myron 5ce5bd1520 fix: reseller creation and management in admin panel
- 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>
2026-06-10 14:44:51 +00:00
myron 008658e0ec feat: expand Docker app catalog from 60 to 140 apps
Added 80 new apps across 13 categories: AI/LLM (Ollama, Open-WebUI,
Flowise, Langfuse, AnythingLLM, LocalAI, ComfyUI), Dev Tools
(code-server, Jenkins, SonarQube, Vault, MailHog, Dozzle, Yacht,
Semaphore), Databases (Redis, MongoDB, PostgreSQL, MariaDB,
Elasticsearch, InfluxDB, Neo4j, Qdrant), Monitoring (Loki+Grafana,
Jaeger, VictoriaMetrics, changedetection.io, Metabase), Networking
(Pi-hole, AdGuard Home, NPM, Traefik, wg-easy, Cloudflared, CrowdSec,
Authelia), CMS/Commerce (Drupal, Joomla, Grav, PrestaShop, OpenCart,
Medusa, Answer), Project Mgmt (Plane, OpenProject, Leantime, WeKan,
Focalboard), Communication (Matrix Synapse, Gotify, ntfy, Grist,
Mailpit), File/Storage (Syncthing, SFTPGo, ownCloud, Duplicati,
Calibre-Web), ERP/Business (Odoo, Akaunting, Monica, Twenty CRM,
Dolibarr), Media (Audiobookshelf, Komga, Grocy, TubeArchivist, AFFiNE),
Smart Home (Home Assistant, Node-RED, Mosquitto, Zigbee2MQTT),
Dashboards (Dashy, Homarr, Homer, IT-Tools, CloudBeaver, pgAdmin,
phpMyAdmin, Infisical).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 13:34:31 +00:00
myron 9bc427f8a2 feat: per-stack Reinstall + fix stack ownership enforcement
- 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>
2026-06-10 13:01:01 +00:00
myron 7a42be8d01 feat: Docker catalog in admin panel + per-account app removal
- 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>
2026-06-10 12:58:43 +00:00
myron 61329ff343 feat: expand Docker app catalog from 21 to 60 apps
Adds 39 new apps across 8 categories:
- Media & Files: Jellyfin, Navidrome, Kavita, Paperless-ngx, File Browser, Seafile, Immich
- Monitoring & DevOps: Adminer, Grafana, Prometheus, Netdata, Glances, Healthchecks, Docker Registry, Verdaccio, Watchtower
- Git & CI/CD: Forgejo, Woodpecker CI
- Knowledge: BookStack, HedgeDoc, FreshRSS, Wallabag, Homepage
- Auth & Security: Keycloak, Authentik, Passbolt CE
- Analytics: Plausible (Postgres + ClickHouse)
- Low-code / No-code: Baserow, Appsmith CE, NocoDB
- Communication: Rocket.Chat, Chatwoot, Zammad
- Business: Invoice Ninja, Linkding, Mealie
- Design: Penpot, Excalidraw, Stirling PDF

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 12:55:35 +00:00
myron 2cc219f9fa feat: add 12 Docker app catalog entries
Adds Uptime Kuma, Portainer CE, MinIO, n8n, Directus, Listmonk, Umami,
PhotoPrism, Meilisearch, Wiki.js, Vikunja, and Mattermost — each with
catalog metadata and a complete docker-compose template.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 12:45:30 +00:00
myron 2fa1f10901 Security: fix 8 code-review findings
- 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>
2026-06-10 12:32:06 +00:00
myron 658e2f9057 Fix PHP-FPM pool not removed on account termination
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>
2026-06-10 05:53:25 +00:00
myron b9c37030b6 Fix terminate 500: require DatabaseManager before calling drop
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>
2026-06-10 05:45:33 +00:00
myron 57949214de Fix WordPress manager 500: define DB_HOST, lazy-load MySQL provDb
- 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>
2026-06-10 03:17:39 +00:00
myron 4d7c35076b Fix 10 code review findings: security, correctness, and SQLite compat
- system.php: fix null dereference on fetchOne (TypeError on null['value'])
- system.php: validate update_channel to ['stable','beta'] to prevent shell injection
- system.php: escapeshellarg remoteBranch in git log/show calls (was RCE vector)
- system.php: fix backup path — rsync contents, not directory, so restore is symmetric
- system.php: syntax check only changed files (git diff) not all 300+ panel files
- system.php: copy VERSION to $webRoot/VERSION not $webRoot/../VERSION (wrong path)
- system.php: fix 3× ON DUPLICATE KEY UPDATE → SQLite ON CONFLICT syntax
- deploy-runner.sh: hoist DB_PATH/CHANNEL above while loop
- deploy-runner.sh: sanitize NEW_VERSION and commit hashes before SQL interpolation
- deploy-runner.sh: parse queued branch (4th field) from webhook queue entry
- webhook.php: remove dead $branch config variable
- webhook.php: include pushed branch in queue entry to eliminate TOCTOU race

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 03:06:14 +00:00
myron 14aa6e8b4d Fix column name: commit_hash → git_commit in novacpx_version INSERT
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:55:43 +00:00
myron 9cabe8af5e Wire update channel (stable/beta) into settings, check, deploy, and version tracking
- 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>
2026-06-09 22:44:46 +00:00