Commit Graph
13 Commits
Author SHA1 Message Date
Divarion_D 4ecb9db739 feat(modules): show modules by their module.json title
The Modules page, dependency warnings and job messages named modules by
`name` — the code identifier (`billing`) that keys config/modules.php, the
class namespace and dependencies. module.json now takes an optional
`title` for people (the store writes its listing name there); the page
shows it with `name` under it, and everything still keys off `name`.
Without a title the name is shown, as before.
2026-10-13 20:39:31 +03:00
obscuremindandClaude Opus 5.5 c2a10a4f83 fix: permission gates, secrets and a shared state file, from the scan's improvements
- The global search listed every kind of record, with its details, to
  every admin group: it asks only for the kinds the group's pages show.
  The load graph takes the dashboard's or the servers' permission, and the
  category templates' actions (create, save, delete, apply to every
  stream) the page's: they ran for any signed-in group.
- get_settings gave every setting to any API token that may read, the
  stream tokens' key and the Redis password among them. A token that is not
  a full one gets none of the secrets a load balancer's replica is not
  given either, nor the metrics token and the reminders' webhook.
- config/modules.php was read, changed and written back whole: of two
  processes changing it at once the second undid the first's change. A
  manager writes what it changed since it read the file onto the file as it
  is now, one writer at a time.
- The bouquets were read again for each of a line's bouquets (EPG API), and
  the categories for each stream of a reseller's guide.
- A NULL in a column read as a JSON list ended a table, a search or a save
  (count() or foreach over null): read as an empty list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:15:12 +00:00
obscuremindandClaude Opus 5.5 f82a574b0c fix(core): transactions over DDL, module retries and archives, image cache
Leads of the 2026-10-10 scan, re-read and confirmed:

- transactional() over a DDL statement: the statement commits on its own,
  PDO then refused the commit and the rollback, so a module migration that
  had done its work threw "There is no active transaction", the callback's
  own error was hidden, and the handler stayed "in a transaction" for the
  rest of the process. commit() and rollback() leave the transaction first
  and ask PDO only for one that is still open.
- A module updated at the second attempt stayed Failed, and unloaded.
- A module archive is told apart by its content: a stored one is named
  .zip whatever it is and an upload has no extension, so a tar.gz update
  could not be extracted again or fetched by a load balancer, and a plain
  .tar upload failed.
- A 404 page, a redirect's body or a page answered 200 was cached under an
  image's name and never fetched again: only a 2xx answer that is an image
  is cached.
- A prebuffer shorter than a segment returned the whole playlist
  (array_slice(..., -0)).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:16:08 +00:00
Divarion_D 99f9bb1dd6 feat(modules): carry an install over when a module is renamed
A release whose module.json keeps the hash_id but changes the name used to land
beside the old copy: both booted, and the new name started with no state or
installed_version. Placing a module now drops any other copy with the same
hash_id and moves its config/modules.php entry to the new name; a git update
renames the directory and entry it wrote into.
2026-10-10 16:54:18 +03:00
Divarion_D 8d01182567 feat(modules): install no module by itself
config/bundled_modules.php listed Watch and Plex, and every
`console.php status` (install, each service start, each update)
downloaded whichever of them was not on disk: an operator could not
decline one, and a deleted module came back at the next restart. The
list and its provisioning (getStandardSet, provisionStandardSet,
installModuleFromSource, findModuleByHashId) are gone; syncBundledModules()
installs only modules already on disk. A module reaches the disk when
the operator installs it. A recovery bundle no longer carries the file;
an older one that does still opens.
2026-10-08 18:24:08 +03:00
Divarion_D be115bd053 feat(modules): enforce requires_core when loading, installing and updating
`requires_core` was only read and shown in the Modules table. A module
release that needs a newer core could be installed or updated anyway and
take the panel down, the update check offered such releases, and modules
left behind by a core rollback kept loading.

ModuleLoader::coreRequirementError() checks a manifest's constraint
(one or more `>= > <= < = == !=` conditions, a bare version meaning >=;
empty = any core; anything else unreadable and refused) against
XC_VM_VERSION, a nightly `X.Y.Z-dev.N` counting as X.Y.Z. It is applied:

- on load: the module is skipped with a log line, its dependents with it;
- on install (placeModuleFiles: zip, LB, standard set, platform) and on a
  git/url update: refused before the installed copy is touched; a refused
  copy the platform extracted into the modules directory is removed;
- in the update check: git offers the newest release whose module.json
  (repository root, read from raw.githubusercontent.com, up to 10 tags)
  this core can run, url honours requires_core in version.json;
- in the Modules table: the reason on the existing warning badge, now
  labelled "Issue(s)".

The checks sit in small helpers, so discoverModules() and
updateModuleFromSource() come out simpler than before.
2026-10-04 11:59:43 +03:00
Divarion_D aab782b9c2 fix(modules): stop a reinstall from switching a disabled module back on
installModule() ended with an unconditional setState(Enabled). Store updates,
zip uploads and plain reinstalls all pass through it, so switching a module off
in the panel survived only until its next update — which is how a disabled plex
came back and then blocked `watch` from being disabled at all.

The admin's choice is now captured before the Installing transition overwrites
it and restored at the end; a first install still enables, since there is no
choice to preserve. stateAfterInstall() reads the raw override rather than
listModules() so the transient Installing / Failed states cannot be mistaken
for a deliberate "disabled".

Restoring that state uses the new writeState() instead of setState(): putting a
module back into the state it already had is a lifecycle transition, not an
admin "disable", and must not be vetoed by the dependents guard — otherwise
updating a disabled `watch` would fail at the last step because `plex` exists.
setState() keeps the guard for the deliberate case.

Three tests cover it; two of them fail against the previous behaviour.

Verified: 860 tests, make gates, CRAP gate.
2026-09-22 20:56:43 +03:00
Divarion_D adb36d8a5b fix(modules): unblock disabling, contain lifecycle failures, keep one dir per module
Disabling a module whose own dependency was already off was impossible: the
guard counted a dependent's nominal Enabled state, while ModuleLoader prunes
unsatisfiable modules transitively. A dependent sitting on an already-broken
chain is not running, so it must not block. The guard now mirrors the loader's
prune and only counts dependents that would genuinely stop working.

Alongside that, three lifecycle holes found while reviewing ModuleManager:

- updateModule() had no failure handling and no incremental watermark. A
  migration failing half-way left the module Enabled at the old version, so a
  retry replayed already-applied deltas — a non-idempotent one then failed
  forever. File deltas and programmatic migrations are now merged into one
  ascending timeline, the recorded version advances per COMPLETED version, and
  a failure marks the module Failed. ModuleMigrator gains pending() and a
  public applyFile(); up() is reimplemented on top of them, unchanged.

- uninstallModule() had no try/catch, unlike installModule(). A failing
  uninstall() hook left the module installed and Enabled with half-deleted
  data. It now marks the module Failed. The duplicated dependents guard moves
  into assertNoInstalledDependents(), shared with deleteModule().

- Platform (store) installs assumed a bare modules/{slug} directory while every
  other path uses {name}_{hash5}. The backup step then found nothing and the
  pulled copy landed beside the existing install, leaving two manifests for one
  module. The platform flow now reuses placeModuleFiles(), which drops rival
  copies and moves the source into place instead of copying it; listModules()
  de-duplicates by name so a stray copy cannot show the module twice.

archivesPath now sits beside modulesPath rather than resolving under MAIN_HOME,
so an injected modules path is honoured (identical path in production).

Verified: 852 tests, make gates, CRAP gate.
2026-09-22 16:21:51 +03:00
Divarion-D a8c1c0f2cc test(modules): dir convention, legacy migration, loader resilience, install
- ArchitectureTest/InterfaceContractTest resolve modules via module.json instead
  of the directory basename (works with {name}_{hash5}).
- ModuleLoaderTest: loads from a hash-suffixed dir; a broken module and its
  dependents are skipped without aborting the whole load.
- ModuleManagerMigrationsTest: legacy bare -> hashed migration (+ stale-dup drop),
  and install of a module living in a hash-suffixed directory.
2026-07-03 20:12:01 +03:00
Divarion-D 2995d69b4d fix(modules): improve dependency handling and diagnostics for module loading 2026-06-30 22:41:46 +03:00
Divarion-D e8b7ab8b2e chore(psr4): phase 3 (Core) — namespace Core/Container
Move Core/Container into XcVm\Core\Container (ServiceContainer) and
Core/Container/Psr into XcVm\Core\Container\Psr (ContainerInterface,
ContainerExceptionInterface, NotFoundExceptionInterface, NotFoundException).

- Namespace ServiceContainer + the 4 PSR-11 interfaces/classes.
- ServiceContainer: import the PSR siblings (use ...\Psr\ContainerInterface,
  NotFoundException); migrated deps (DatabaseHandler/ModuleInterface/Request/
  SettingsManager) keep their use; still-global exceptions/decorators qualified
  with leading backslash (\ContainerException, \XcVmException, \Exception, ...).
- Rewrite 'use ServiceContainer;' → FQCN (incl. module fixtures); add use to
  referrers across src/ and tests/; fix leading-backslash \ServiceContainer refs.
- Core/Exception/Container/ContainerException: import the moved PSR
  ContainerExceptionInterface.
- Router: class_exists('ServiceContainer') → ::class. InterfaceContractTest boot
  param-type → FQCN. Repaired use-inserter mis-placement in the two namespace()
  fixture tests.
- phpstan-baseline.neon regenerated (292→292).

Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; ServiceContainer
resolves and implements XcVm\Core\Container\Psr\ContainerInterface.
2026-06-24 22:13:35 +03:00
Divarion-D 881991e52b chore(psr4): phase 3 (Core hubs) — namespace Core/Module
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.
2026-06-24 22:05:10 +03:00
Divarion-D 0d8607b17b test: add comprehensive test suite covering all P2–P4 audit phases (242 tests)
New test files:
- ModuleLoaderBootTest — 11 tests: boot lifecycle, event subscribers, BaseModule compat
- ModuleManagerMigrationsTest — 8 tests: versioned migrations, semver ordering, install/uninstall state
- ModuleStateEnumTest — 12 tests: fromRaw(), isLoadable(), ModuleManager integration
- ComposerDiscoveryTest — 7 tests: Composer 1/2 format, xcvm-module filter, deduplication, module-path
- CronProviderTest — 8 tests: BaseModule default, collectCronEntries aggregation, format validation
- ExceptionHierarchyTest — hierarchy correctness, PSR-11 NotFoundException compliance
- InterfaceContractTest — 29 reflection tests verifying all documented interface signatures
- ListensToAttributeTest — 8 tests: attribute shape, ModuleLoader integration, IS_REPEATABLE, hybrid mode
- ArchitectureTest — enforces no global \$db in modules or domain, BaseModule contract

Updated ModuleLoaderTest and ModuleLoaderPriorityTest for namespaced class
generation and resolveClassName() removal.
2026-06-15 18:07:50 +03:00