Commit Graph

114 Commits

Author SHA1 Message Date
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
myron 846683c7a2 Add SEO meta description, keywords, and noindex to all three panel pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:30:53 +00:00
myron f9d423b15a Fix OS upgrade script: date format and backup dir permission
- 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>
2026-06-09 22:24:50 +00:00
myron 09bd0820a5 Updates page: serve cached results instantly, nightly cron refreshes cache
- 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>
2026-06-09 22:23:45 +00:00
myron 877f157665 Docker page: show compose stacks in My Apps tab instead of raw containers
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>
2026-06-09 22:18:39 +00:00
myron 1ac9728fd7 Fix Docker async launch, email SUBSTRING_INDEX (SQLite), postfix sudo writes
- 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>
2026-06-09 22:15:41 +00:00
myron 24a2434ccd Email create modal: split input into local-part + domain dropdown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:13:29 +00:00
myron dc8829e2c9 Fix DockerManager stack directory creation using sudo mkdir for www-data
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:09:55 +00:00
myron 89996cc0ea Fix _branding.php session lookup using correct column name (id not token)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 22:04:18 +00:00
myron b90f753890 Add version badge to reseller and user panel sidebars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 18:57:48 +00:00
myron ddd81d73e7 Fix MySQL create: sanitize db names, fix empty db_user default, catch RuntimeException
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>
2026-06-09 18:42:24 +00:00
myron c22e1fd067 Fix multiple user panel 500 errors
- 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>
2026-06-09 18:32:10 +00:00
myron 8179326526 Fix NOVACPX_ROOT undefined on panel pages (black screen)
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>
2026-06-09 18:22:05 +00:00
myron 1c2c11251c Streaming terminals for PHP extensions, SSL certificates; UFW logging state fix
- php.php: install-extension and remove-extension now stream via SSE (real-time progress, proper error detection, sudo)
- ssl.php: issue action now streams certbot output via SSE
- admin.js: phpExtInstall/Remove use streaming terminal modal
- admin.js: adminIssueBulkSSL uses streaming modal with per-domain progress
- admin.js: adminRenewCert now confirms before renewing
- admin.js: adminIssueSingleSSL helper for per-domain streaming SSL
- admin.js: firewall page pre-selects current UFW logging level from API response
- admin.js: fwSetLogging reloads firewall page on success
- firewall.php: ufw_status() now parses and returns logging level

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 18:04:43 +00:00
myron 4d016b4156 Add notification email templates: DB migration, API CRUD, admin UI
- db/migrations/009_email_templates.sql: email_templates table with 8 default templates
- db/schema.sql: email_templates table added
- system.php: email-templates/get/save/delete/test actions with placeholder rendering
- admin.js: notifications page enhanced with template list, edit modal, CRUD, send test
- Templates support placeholders: {{name}}, {{domain}}, {{username}}, {{password}}, etc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:59:54 +00:00
myron 2af9e34fb0 Add service versions panel, version auto-tracking, Fail2Ban sidebar, streaming service switch
- .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>
2026-06-09 16:23:51 +00:00