Ministra stops being a module — the whole Stalker portal (portal.php,
MinistraBootstrap, PortalHandler/PortalHelpers and the STB front-end) now
lives in src/Ministra/ under the XcVm\Ministra namespace, served at
/home/xc_vm/Ministra via the nginx alias.
- src/ministra/* and Modules/ministra_85a7d/{PortalHandler,PortalHelpers}
→ src/Ministra/; MinistraModule.php + module.json removed. Ministra was
the only committed module, so src/Modules/ keeps a .gitkeep.
- portal.php resolves PortalHandler as a sibling and derives MAIN_HOME from
its new location (glob crutch gone).
- nginx alias + AuthRepository $rAlias switched to /home/xc_vm/Ministra
(PascalCase); ministra entry dropped from bundled_modules.php.
- Makefile: Modules/ removed from LB_DIRS — all modules are MAIN-only, so
the ~50 MB of portal assets no longer ship to LB nodes.
- ArchitectureTest: zero committed modules is now a valid state.
- PHPStan: analyse src/Ministra, exclude the procedural portal.php entry,
repath the ministra baseline entries.
- Docs (architecture, ministra-browser-emulation, extraction plan) updated
to the new layout; the "extract to a separate repo" plan is cancelled.
Verified: php -l, make gates, make phpstan (No errors), full unit suite
(432 tests). On-server smoke: handshake + get_profile work end-to-end with
a registered MAC after deploy.
BoundaryInterface was a marker interface with no runtime consumer —
nothing read getEntryPoint()/isIsolated() and, being static-less
metadata, it enforced nothing. Its only implementor was MinistraModule.
Removes the interface, drops `implements BoundaryInterface` plus the two
orphaned methods from MinistraModule (getName/getVersion stay — they come
from BaseModule/ModuleInterface), and deletes the now-empty Core/Boundary/.
Test contract (InterfaceContractTest) loses the three BoundaryInterface
assertions; docs (en/ru architecture + modules, .github instructions) now
describe isolated subsystems like Ministra as a convention — own entry
point + bootstrap — rather than a marker interface.
Verified: php -l, make gates, make phpstan (No errors);
InterfaceContractTest + ArchitectureTest green (33 tests, 96 assertions).
The panel is deeply coupled to TMDb (VOD import, player metadata, admin
search, two crons), so shipping it as an uninstallable module only added
failure modes: after the move to hash-suffixed dirs (tmdb_f4e6e) every
hardcoded `Modules/tmdb/lib/...` require broke, and 2.3.3 crons died with
"Failed opening required TmdbClient.php".
tmdb -> core:
- Vendored \TMDB client -> src/Infrastructure/Tmdb/lib/; the only loader
is TmdbApiService::requireLibrary() (now public, also loads Release.php).
- TmdbApiService -> XcVm\Infrastructure\Tmdb — composer-autoloaded in every
bootstrap context, no module boot required (player scope never booted
modules, so module-namespace classes were unreachable there).
- TmdbCron / TmdbPopularCron -> XcVm\Domain\Vod; cron jobs -> Cli/CronJobs
(picked up by the console.php scan; command names cron:tmdb and
cron:tmdb_popular are unchanged).
- TmdbController -> Public/Controllers/Admin; tmdb_search / tmdb api
actions registered in routes/admin.php (same dispatchApi fallback).
- Domain/Vod services and player_functions.php load the lib through
TMDbService::requireLibrary() instead of hardcoded module paths.
- tmdb removed from config/bundled_modules.php. ModuleLoader gains
CORE_PROVIDED_MODULES: released watch/plex archives still declare
"dependencies": ["tmdb"] — such deps are stripped during manifest
normalization and in ModuleManager::listModules().
- syncBundledModules() purges stale on-disk tmdb module dirs and their
config/modules.php state on upgraded panels, so the old copy cannot boot
alongside the core implementation and collide on command names.
Standard-set provisioning fix (root cause of the "Undefined variable $db"
errors from watch/plex settings views on 2.3.3):
- Production still ran watch_e6c86/plex_20cd9-less legacy copies migrated
from 2.3.2 with generated hash_ids; provisionStandardSet() treated any
same-name directory as "already on disk" and never fetched the pinned
1.0.2/1.0.1 releases that contain the fix. A same-name directory whose
identity does not match the pinned hash_id is now considered stale: it
is deleted and the pinned release is installed in its place.
- installModuleFromSource(): when the module is already recorded as
installed (files re-provisioned over a stale copy), run updateModule()
(incremental from->to migrations) instead of re-running the initial
install.
watch and plex now live in their own repos and are fetched from GitHub releases
instead of shipping in the panel archive:
- bundled_modules.php: their standard-set entries switch source bundled -> git
(Vateron-Media/Module_Watch @ 1.0.2, Vateron-Media/Module_Plex @ 1.0.1).
- Remove src/Modules/watch_2541a and plex_20cd9 from the panel tree.
On install/status, provisionStandardSet() downloads each module's release
(module.tar.gz + hashes.md5), verifies the md5 and the pinned hash_id, and
installs it — in registry order, so tmdb (bundled) and watch land before plex.
deleted_files.txt captures the removed paths at release time.
Note: core/domain code still hard-references these module classes (e.g.
Domain\Vod\MovieService calls WatchService::getWatchCategories), so the standard
set must be provisioned for VOD import etc. to work — a pre-existing coupling,
now also a fetch-at-install dependency.
Rename watch->watch_2541a, plex->plex_20cd9, tmdb->tmdb_f4e6e,
ministra->ministra_85a7d, where the suffix is the first 5 chars of each
module's hash_id. The logical name (module.json "name", never containing "_")
stays canonical, so config keys, namespaces and dependencies are unchanged —
only the on-disk directory carries the hash. This lets same-named modules
coexist on disk. deleted_files.txt captures the old paths at release time
(make generate_deleted_files, --no-renames).
Mirror core's DB layout at the module level: one master database.sql (full
current schema), one database_drop.sql (teardown), and forward-only
migrations/<semver>.sql deltas. ModuleMigrator gains install()/uninstall();
up() reads migrations/*.sql; down() removed.
- watch: database.sql (delete_missing folded into watch_folders),
database_drop.sql, migrations/1.0.1.sql; drop 1.0.0.up/down.sql;
getVersion() -> 1.0.1 (matches module.json).
- Move core migration 004_add_watch_delete_missing.sql into the watch module.
- installModule no longer wraps the DDL migration in a transaction: MySQL/MariaDB
implicitly commit DDL, so the wrapping rollback() only masked the real SQL error.
- syncBundledModules retries a previously-failed install (skips only admin-disabled)
so `console.php status` self-heals.
Make the bundled watch/plex modules own their database schema and lifecycle
instead of being hard-wired into the base schema, and decouple core code from
module-owned tables.
File-based module migrations:
- ModuleMigrator runs Modules/<name>/migrations/<semver>.up.sql / .down.sql,
ordered by semver; installed_version (config/modules.php) is the watermark.
- Wired into ModuleManager: install → up(null→target), update → up(from→target),
uninstall → down(installed). Install is transactional (a failing migration
rolls back and marks the module Failed).
- watch ships 1.0.0.up/down.sql (creates/drops watch_categories, watch_folders,
watch_logs, watch_refresh + seeds the default TMDb genre categories with
INSERT IGNORE). Removed those tables from bin/install/database.sql — the
module is now the single source of that DDL.
Module lifecycle:
- syncBundledModules() auto-installs never-installed bundled modules in
dependency order; called from StatusCommand after MigrationRunner::run so a
fresh install/update provisions their tables. Idempotent.
- uninstallModule() refuses to remove a module that installed dependents still
require.
- plex declares "dependencies": ["watch"], owns no tables, and on uninstall
deletes only its own rows (watch_folders type='plex', categories 3/4).
Decouple core from watch tables via events (core would otherwise fatal once the
tables are droppable):
- New StreamsDeletedEvent / BouquetDeletedEvent, dispatched from
StreamRepository::deleteStream/deleteStreams, ToolsCommand::deleteStreams and
BouquetService::deleteById in place of direct DELETE/UPDATE on watch tables.
- WatchModule subscribes via #[ListensTo] and delegates to WatchService, which
now holds the watch_refresh/watch_logs/watch_folders cleanup. Fixes a latent
bug where bouquet deletion wiped fb_bouquets (column was read but not SELECTed).
- Moved TMDbService::updateCategories() (writes watch_categories) into
WatchService; it now builds the client via the PSR-4
TmdbApiService::createClient() instead of a manual require of the legacy lib
(createClient is now self-contained).
Replace the fragile per-class setDb()/db() pattern (which threw when a
bootstrap path forgot to wire $db) with a shared trait that lazily resolves
from DatabaseFactory. Fixes the streaming UserRepository fatal and the same
latent issue in module crons. 42 classes converted.
- Exclude bundled third-party code from analysis (Modules/tmdb/lib/*,
Core/Util/MobileDetect.php) — like the vendored Composer packages, these
are not our code (php-tmdb client, Mobile-Detect). Removes 67 baselined
errors incl. the only class.notFound and phpDoc.parseError (both vendored).
- return.void (10): drop the discarded return value (return true/false/null →
return;) in void methods — NetworkUtils, LineService, StreamProcess, TMDbService.
- deadCode.unreachable (2): remove the dead `return;` after AdminHelpers::goHome()
(a never-returning call) in Plex/WatchController.
- Regenerate phpstan-baseline.neon: 440 -> 361 errors (291 -> 241 blocks).
Verified: make phpstan green, PHPUnit 298/298.
The PSR-4 migration's leading-backslash qualification leaked into string
require paths, turning 'Modules/tmdb/lib/Release.php' into
'Modules/tmdb/lib/\Release.php' → "Failed to open stream: No such file or
directory" at runtime (TmdbCronJob, TmdbCron, watch/WatchItemCommand). Remove
the stray backslash in all three. PHPStan can't catch this (constant + string
literal concatenation in require), so it surfaced only at runtime.
PHP string class names are always fully-qualified (they ignore the current
namespace and use-imports), so several short-name string references silently
broke when their classes were namespaced — failing only at runtime.
- StreamUtils::sortArguments(): the uasort() callback array('StreamUtils',
'customOrder') referenced the now-namespaced class by its old global short
name -> "class StreamUtils not found". Use array(self::class, 'customOrder').
- Router::checkPermission(): `class_exists('Authorization')` was always false
(Authorization is now XcVm\Core\Auth\Authorization), so the ['type','key']
permission branch never called check() and fell through to `return true` —
granting access unconditionally. Call Authorization::check() directly.
- TmdbController::hasMediaPermission(): `class_exists('Authorization')` always
false -> method always returned false (media permissions always denied). Drop
the dead guard; the call already imports the FQCN.
- AdminLogoutController: `class_exists('SessionManager')` always false -> the
admin session was never cleared on logout. Call SessionManager::clearContext()
directly.
- BruteforceGuard::getDB(): `class_exists('DatabaseFactory', false)` always
false -> qualify to \XcVm\Infrastructure\Database\DatabaseFactory::class.
Verified: php -l clean; StreamUtils callback is callable; make gates pass;
PHPUnit green (incl. StreamUtilsTest).
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 the modules' own classes into XcVm\Module\<Pascal> (Plex, Watch, Tmdb,
Ministra) — PlexController/PlexService/PlexCron/..., WatchController/WatchService/...,
TmdbController/TmdbCron/TmdbApiService/..., PortalHandler/PortalHelpers (~23 classes).
They now resolve through the Phase-2 ModuleLoader PSR-4 resolver.
- The bundled third-party tmdb/lib/* (TMDB client, Entities, roles, config) stays
GLOBAL, like the other vendored libs; namespaced Tmdb classes reference it via
\TMDB etc.
- Rewrite the *Module classes' (and any sibling) 'use ShortName;' imports → FQCN;
add use to referrers; convert leading-backslash refs. Sub-classes keep the
Core/Domain/Cli use imports added during those layers.
- Qualify built-ins (\DateTime, \Exception, \PDO, ...) and the global lib classes.
- phpstan-baseline.neon regenerated.
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; all 4 modules load and
their sub-classes (e.g. XcVm\Module\Plex\PlexService) resolve via the module
PSR-4 autoloader; re-sweep clean.
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/Module into XcVm\Core\Module (BaseModule, ModuleInterface,
MigratableInterface, ModuleLoader, ModuleManager, NavbarRegistry, NavbarItem,
CoreNavbarProvider) and Core/Module/Contract into XcVm\Core\Module\Contract
(the 6 provider interfaces). Completes the Core hub layer.
- Namespace 14 files (8 root + 6 Contract). Root files import the contracts via
'use XcVm\Core\Module\Contract\...'; NavbarProviderInterface imports the
root NavbarRegistry. Qualify still-global deps with leading backslash
(\ServiceContainer, \CommandRegistry, \ModuleState, \ServerEnvironment,
\EventDispatcher, \ListensTo, the Module exceptions, \ZipArchive,
\InvalidArgumentException, \RuntimeException) — Container/Events/Enum/Exception
migrate later. Migrated deps (Router, StreamPipeline, ...) keep their use.
- Rewrite the modules' 'use BaseModule/NavbarRegistry/NavbarItem;' → FQCN; add
'use XcVm\Core\Module\...;' to other referrers across src/ and tests/.
- Fix pre-existing leading-backslash refs to the FQCN.
- Tests: qualify the module fixtures' generated 'use ModuleInterface;' /
'use BaseModule;' / 'use NavbarRegistry;' and the registerRoutes type hint to
FQCN; add real top-level use imports to the fixture-builder tests; update
InterfaceContractTest registerNavbar param-type to the FQCN. Repaired
use-inserter mis-placements in the two tests that declare a namespace() method.
- Public/index.php: class_exists('ModuleLoader') → class_exists(ModuleLoader::class).
- phpstan-baseline.neon regenerated (292→292; no new/unknown-class errors).
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; the 4 real modules
load and resolve as XcVm\Core\Module\ModuleInterface; leading-backslash
re-sweep across all migrated classes is clean.
Move Core/Http into XcVm\Core\Http (ApiClient, CurlClient, RequestManager,
Request, Response, Router) and Core/Http/Pipeline into XcVm\Core\Http\Pipeline
(StreamContext, StreamMiddlewareInterface, StreamPipeline). RequestGuard.php is
procedural (global functions) and stays global.
- Namespace the 9 classes; qualify still-global deps with leading backslash
(\ServerRepository in ApiClient; \Authorization, \ServiceContainer,
\AdminHelpers in Router) and built-in \Throwable; same-namespace siblings
unqualified.
- Add 'use XcVm\Core\Http\...;' to referencing files — RequestManager 128,
Router 19, ApiClient 15, Request 11, Response 6, CurlClient 5, plus Pipeline —
across src/ and tests/.
- Rewrite the modules' 'use Router;' → 'use XcVm\Core\Http\Router;' (plex,
watch, tmdb).
- Fix pre-existing leading-backslash global refs (\Router etc.) to the FQCN.
- Tests: fully-qualify the Router type hint in generated module fixtures
(registerRoutes(\XcVm\Core\Http\Router ...)) and update the
InterfaceContractTest param-type expectation to the FQCN. Also repaired two
fixtures where the use-inserter mis-placed a 'use' (files declare a method
literally named namespace(), which the tokenizer reports as T_NAMESPACE).
- phpstan-baseline.neon regenerated (292→292; class names in frozen messages
gained the namespace, no new/unknown-class errors).
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; Http classes resolve
via Composer; leading-backslash re-sweep across migrated classes is 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.