Adds a "safety net" rollback so a server can be downgraded to an earlier
release without a manual redeploy, mirroring the existing update flow.
- GitHubReleases: getVersionFile() fetches a specific version's asset (not
just latest); getPreviousVersions() lists the newest releases older than a
given version, each flagged beta (GitHub prerelease). getReleases() now
delegates to a shared getFilteredReleases() that keeps the prerelease flag.
- UpdateCommand: new `rollback <version>` case — validates the target is a
real, strictly-older release; on MAIN takes an automatic DB backup first
(aborting if it fails); downloads the exact version (main/lb_update asset),
verifies MD5, and hands off to the same python `update` applier.
- RootSignalsCronJob: dispatch the `rollback` signal to `console.php update
rollback <version>` (version regex-validated, escapeshellarg'd).
- api.php: `rollback_versions` (channel-aware list, optional per-server base
version) and server `sub=rollback` (validates version, writes the signal).
- servers.php: per-server "Rollback Version" action (dropdown item + button)
opening a modal to pick a version; beta builds show "(beta)". Works for
MAIN and each LB independently.
- docs: rollback sections in server-update.md and update-system.md.
The downgrade reuses the version-agnostic python applier, so binaries,
config and user data are preserved. Migrations are forward-only and are not
rolled back; the automatic MAIN backup is the recovery path.
Migrate SettingsManager::getAll()['key'] to SettingsManager::get('key') across
CLI commands and cron jobs. get() is used here (rather than the typed
accessors) because it returns the raw value unchanged, preserving the diverse
call sites verbatim: `=== null` checks, `?? ''`/`?? false` fallbacks, string
concatenation, numeric comparisons and ternaries. It still adds a default,
silencing PHP 8 "Undefined array key" warnings. Whole-array `$rSettings =
getAll()` grabs are left as-is.
Hoist the request-invariant redis_handler flag to method entry in
UsersCronJob/StreamsCronJob (loadCron/processDeletions), where it was read deep
inside nested per-connection loops.
Also fix two long-standing PHPStan baseline entries by initialising
$rRedisDelete and $rLiveKeys unconditionally in UsersCronJob (they were only
assigned inside `if ($rRedis)` but read under correlated guards) and drop the
now-stale baseline entries.
The xcvm_core PHP extension is mirrored into the binaries repo tree — decoupled
from the heavy per-distro runtime bundle — under bin/xcvm_core/ as per-OpenSSL-ABI
archives (openssl1.1 / openssl3) + version.json + SHA256SUMS. Nothing panel-side
kept it current, so nodes drifted onto whatever build their install archive shipped
— and critically the LB's build lacked XC_VM::config_set_redis, so an LB could not
point its Redis at the main server and every redis_handler connection failed with
LINE_CREATE_FAIL.
Add XcvmCoreCommand (console.php xcvm_core), modelled on FanoutBinaryCommand:
- resolves the latest version from version.json (raw, main branch);
- detects this host's OpenSSL ABI (libcrypto.so.3 -> openssl3, else openssl1.1;
both present -> prefer openssl3 with a load-test fallback to the other);
- downloads the matched archive, verifies SHA-256;
- installs the .so ATOMICALLY with a backup + fresh-php load-test + rollback, so a
wrong-ABI/broken extension never takes php-fpm down, then USR2-reloads php-fpm;
- version-compared via a sidecar marker next to the .so (idempotent).
Wired into UpdateCommand (post-update) and RootSignalsCronJob (hourly self-heal),
not LB-stripped, so every node converges. Proven on the canary LB: none -> 2.1.0
(openssl3), config_set_redis appears, StatusCommand::configureRedisLb points Redis
at main, RedisManager connects, and a viewer redirected to the LB is served
video/mp2t by the daemon (/connections + /rates populated, php-fpm flat at 1).
Nothing pulled or bootstrapped the daemon: the panel archive doesn't ship the
binary (it's a GitHub-release asset via `fanout_binary`), and `fanout_binary`
was invoked by no installer, updater or cron. So a fresh node/LB never got the
daemon, and an updated panel kept an old one. Wire it up:
- UpdateCommand: after a panel update, run `console.php fanout_binary` (root,
background, best-effort) so the daemon is refreshed to match the new version.
- RootSignalsCronJob (root, every node incl. LBs): hourly self-heal — throttled
`fanout_binary` via a stamp, first pass immediate, so a fresh install/LB gets
the daemon within a minute and version drift is corrected.
- service: the keepalive loop now runs UNCONDITIONALLY and re-checks for the
binary each 2s pass instead of gating the whole block on `-x` at boot. This
closes the bootstrap gap — fanout_binary only pkills and relies on a running
keepalive to respawn, which didn't exist on a node whose binary arrived after
boot; now the loop launches the daemon within ~2s of the binary appearing.
fanout_binary is idempotent (downloads only on a version mismatch, only when
GitHub is reachable), so polling is safe. php -l + make gates green.
Apply 'make cs-fix' — 493 files. Mechanical, import-block only:
- sort use statements alphabetically (class/function/const grouped);
- drop imports left unused by the PSR-4 migration (e.g. classes referenced by
leading-backslash FQCN whose redundant 'use' the automated insertion had added);
- one blank line after namespace and after the import block; collapse stray
blank lines around use.
No logic changes. Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; and a
temporary PHPStan pass over src/Public/Controllers confirms no still-referenced
import was removed (0 unresolved classes). 'use' after inline HTML in view
templates is valid and aliases correctly (verified) — those imports are sorted too.
Namespace all 55 Cli classes into XcVm\Cli (CommandInterface, CommandRegistry,
CronTrait, DaemonTrait), XcVm\Cli\Commands (28) and XcVm\Cli\CronJobs (23);
migration_logic.php stays procedural/global.
- console.php: switch command discovery from basename==classname + manual require
to FQCN resolution — each scan dir maps to its PSR-4 namespace, classes load via
Composer, implementsInterface(CommandInterface::class). Drop the manual
CommandInterface/CommandRegistry requires. This is the atomic switch the plan
required to land with the Cli namespacing.
- Commands/CronJobs get 'use XcVm\Cli\CommandInterface;' (implements) and the
trait imports 'use XcVm\Cli\CronTrait;'/'DaemonTrait;'; rewrite the modules'
'use CommandRegistry;' → FQCN; qualify built-ins/global (\ReflectionClass,
\PDO, \Exception, \RuntimeException, \XC_Autoloader, \XC_VM).
- Fixtures: 'use CommandRegistry;' → FQCN; InterfaceContractTest registerCommands
param-type → FQCN. phpstan-baseline.neon regenerated.
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; FQCN discovery resolves
51 classes (49 implement CommandInterface); no mangled FQCNs; re-sweep clean.
Move Core/Database into XcVm\Core\Database (DatabaseHandler, Database,
MigrationRunner, QueryHelper). First of the Core hub sub-layers.
- Namespace the 4 classes; DatabaseHandler extends Database (same namespace);
built-ins/ioncube qualified (\PDO, \PDOException, \Exception, \Throwable,
\XC_VM); Database keeps its 'use XcVm\Core\Logging\FileLogger;'.
- Add 'use XcVm\Core\Database\...;' to referencing files (DatabaseHandler 51,
Database 64, QueryHelper 29, MigrationRunner 3) + 2 test files (PHPStan does not
analyse tests/, so PHPUnit is the gate there).
- Rewrite pre-existing leading-backslash global refs (\DatabaseHandler etc., e.g.
in @param docblocks of ResellerApiDispatcher/ResellerTableRenderer) to the full
FQCN \XcVm\Core\Database\... — a 'use' import does not cover a leading-\
reference. Done with a lookbehind so FQCN continuations and use-lines are intact.
- phpstan-baseline.neon regenerated (292→292; pre-existing Database/migration_logic
findings re-anchored after class names in messages gained the namespace).
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295.
Move the last two Core/Config classes into XcVm\Core\Config. SettingsManager
has the largest fan-out of the whole migration (referenced by ~224 files).
- Namespace SettingsManager (self-contained singleton, no class deps) and
SettingsRepository (\FileCache:: qualified).
- Add 'use XcVm\Core\Config\SettingsManager;' to 224 referencing files and
'use ...\SettingsRepository;' to 12 — call sites (SettingsManager::get(), etc.)
unchanged. Done with a token-based inserter (after namespace/declare/<?php,
idempotent, same-namespace files skipped).
- ToolsCommand::processRecaptcha: drop dead class_exists('SettingsManager') +
method_exists guard (always autoloadable now) → call SettingsManager::clearCache()
directly. This was the only string-literal class reference.
Core/Config is now fully namespaced (ConfigReader, DomainResolver, SettingsManager,
SettingsRepository); the procedural constant files (AppConfig/Binaries/Paths)
remain global by design.
Verified: php -l clean (226 files); PHPStan no errors (baseline unchanged — it is
line-independent so the added use-lines don't disturb it); PHPUnit 295/295; all
Config FQCNs resolve via Composer; sample Public referrers lint-clean.