100 Commits
Author SHA1 Message Date
DanilandGitHub 18d0b75c35 Merge pull request #191 from Vateron-Media/refactor/rector
Rector adoption + reviewed refactor, test coverage, and cs-fix crash fixes
2026-09-13 22:35:15 +03:00
Divarion_D fe0a4f425c Merge remote-tracking branch 'XC_VM/main' into refactor/rector
# Conflicts:
#	src/Core/Process/ProcessManager.php
2026-09-13 22:33:47 +03:00
Divarion_D 29d98939ef build(cs): temporarily mute error-level sniffs for a green baseline
`make cs` fired 2321 errors across legacy code, so it could not act as a
gate. Set the ~20 error-level sniffs to severity 0 in a clearly-marked
TEMPORARY block (and ParameterTypeHint in its own block), and add
ignore_warnings_on_exit so advisory warnings (line length, cyclomatic
"too high", silenced errors) are still reported but do not fail the run.
`make cs` now exits 0.

These mutes are technical debt to unwind ONE sniff at a time (drop the
severity line, run make cs, fix, commit); no new violations should be
added under a muted rule. Muting ParameterTypeHint also stops cs-fix from
re-adding the null-crash param types (see TYPE_AUDIT.md). Biggest buckets:
ParameterTypeHint 1323, GlobalKeyword 452, camelCaps naming 231,
CyclomaticComplexity.MaxExceeded 174.
2026-09-13 22:28:10 +03:00
Divarion_D 942b58cec9 fix(types): accept null in escape/hlsConnectionKey/formatTitle call paths
Continuing the cs-fix null-crash hardening (see TYPE_AUDIT.md):

- ActivityCronJob: `array_map([$db, 'escape'], $rLine)` passed nullable
  row columns straight into Database::escape; cast each value to string
  in the callback so a NULL column no longer fatals.
- ConnectionTracker::hlsConnectionKey: `$rIdentifier` is null for a
  regular (non-HMAC) line — widen to `?string` and coalesce to '' in the
  key (behaviour unchanged: the HMAC branch is only taken when $rIsHMAC
  is set).
- StreamSorter::formatTitle: `$rYear` can be null (VOD rows without a
  year) — widen to `int|string|null` with a null default; the is_numeric
  guard already rejects it.

726 tests green.
2026-09-13 22:09:42 +03:00
Divarion_D 734f4751c9 chore(tests): move phpunit.phar into tests/ and repoint references
The committed PHPUnit runner lived at tools/.bin/phpunit.phar, away from
the suite it runs. Move it next to the tests it drives —
tests/phpunit.phar — and update every invocation to
`php tests/phpunit.phar -c tests/phpunit.xml.dist`:

- CI workflows (ci, build-release, build_pre-release) + the ci.yml header,
- CLAUDE.md, CONTRIBUTING.md, tools/README.md, the qa-lead-reviewer agent,
- docs/en (dev-workflow, updates_checklist, phpunit-phar, refactoring).

docs/ru is generated from docs/en (make docs-translate) and is left for
the next regeneration, per the docs workflow.
2026-09-13 22:04:10 +03:00
Divarion_D 2f4ab6cbbb fix(types): nullable pids in ProcessManager + escape() on refactor/rector
The branch was missing the ProcessManager null-pid hotfix that landed on
main (cb4d12d9); a straight cherry-pick conflicted because Rector had
already reshaped the bodies here (the `(int) $pid` casts were removed).
Re-apply the fix branch-adapted: make all nine /proc pid checks accept
`?int $pid` (isRunning, isNamedProcessRunning, isStreamRunning,
producerKind, resourceSample, kill, getProcessAge, isStreamAlive,
isMonitorAlive) plus `?string $exe`. No cast is needed — each guard is
`$pid <= 0`/`<= 1` and `null <= 0` is true in PHP, so null returns the
safe "not running" result before reaching any /proc access. Docblocks
corrected to `int|null` so a future cs-fix run does not re-narrow.

Also fix Database::escape(string $string) -> ?string with a `(string)`
cast: PDO::quote is strict on PHP 8.1 and callers pass null.

Add TYPE_AUDIT.md tracking the remaining cs-fix null-crash surface
(~237 files) and the per-file hardening process. 726 tests green.
2026-09-13 21:51:33 +03:00
Divarion_D 83aed2e0be test(channel): cover ChannelService massEdit / setOrder branches
ChannelService (the video-channel twin of RadioService) was among the
most Rector-churned untested classes: the pass inverted many empty-else
branches across its server-tree, transcode and flag handling. Lock that
behaviour with characterization tests over the fully-runnable paths
(massEdit has no verifyPostTable/exit; StreamProcess::updateStreams is a
no-op with cache off), exercised through the dual-backend TestDb:

- transcode flag derivation (c_transcode_profile_id -> enable_transcode
  1/0 by sign),
- boolean flag columns (checked => 1, edited-but-absent => 0),
- server-tree ADD (root '#' skipped, 'source' parent stored as NULL),
- setOrder writes a sequential order by the posted id list.

726 tests green.
2026-09-13 21:21:09 +03:00
Divarion_D 52d801acb2 test: dual-backend TestDb (SQLite + MariaDB) + skip-on-panel guards
TestDb can now run the DB-touching unit tests against a real MariaDB as
well as the default in-memory SQLite, so the suite can be exercised on
the panel host (which ships pdo_mysql, not pdo_sqlite). When
XCVM_TEST_DB_DSN is set it connects there and translates the SQLite test
DDL on the fly: AUTOINCREMENT -> AUTO_INCREMENT, a bare INTEGER PRIMARY
KEY gains AUTO_INCREMENT, and each CREATE TABLE is preceded by DROP TABLE
IF EXISTS (a MariaDB schema persists across the per-test connections that
:memory: starts fresh). DDL is routed through exec() on both backends so
the ModuleMigrator path (which runs DDL via query()) works too, and the
MySQL session uses a permissive sql_mode to match SQLite's leniency.
AuthRepositoryTest back-quotes the reserved column `key`.

Three env-fragile guards are tagged #[Group('skip-on-panel')] so the
deployed-panel run can exclude them (--exclude-group skip-on-panel):
ArchitectureTest and StreamTokenCallSitesTest scan the repo src/ tree
(absent / polluted in a flat deploy; they also self-skip when it is
missing), and LoginSessionFixationTest runs in isolated child processes
that the panel's ionCube/OPcache PHP cannot reconstitute.

Verified green on all three backends: SQLite (local, 721), MariaDB 11.4
(container, 721), and the panel's bundled PHP 8.1 + server MariaDB
(713, with the group excluded).
2026-09-13 21:17:06 +03:00
Divarion_D c291aa5266 fix(admin): revert Rector's __DIR__ include rewrite that broke table loads
Rector's FollowRequireByDirRector (applied in the stage-2 pass, 9665a5a2)
rewrote the admin/reseller table bootstrap include:

    include "functions.php";   ->   include __DIR__ . "/functions.php";

The bare include resolved the legacy admin bootstrap via include_path /
CWD at runtime; that bootstrap lives under Public/Views/admin, NOT next
to the controller. Prepending __DIR__ pinned the path to
Public/Controllers/Admin/functions.php, which does not exist, so every
session-authenticated table request (the `isset($_SESSION['hash'])` /
`isset($_SESSION['reseller'])` branch — i.e. the normal browser-panel
path) hit "include(...functions.php): Failed to open stream" and lost the
$db / $rUserInfo / $rPermissions / $rServers globals the rest of index()
needs.

Restore the exact pre-Rector includes and document the rule as unsafe in
build/rector.php so it stays disabled if a future Rector version
reintroduces it. 721 tests green.
2026-09-13 20:29:33 +03:00
Divarion_D b0c77d8eb8 style(phpcs): allow is_null(), keep extract() banned with documented exceptions
Reviewed the Generic.PHP.ForbiddenFunctions list against real usage: the
whole 20-function ban produced only two kinds of violations across src —
is_null (84) and extract (2); the other 18 entries have zero call sites
and stay as free guardrails.

- is_null: dropped from the ban. It is equivalent to `=== null` and the
  prohibition was purely cosmetic; keeping it avoids churning 41 files
  for no functional gain.
- extract: kept banned (it injects variables from array keys — a real
  footgun), but the two legitimate uses in the view-render layer
  (BaseAdminController, BasePlayerController) expose the payload to legacy
  PHP templates and cannot be removed without rewriting every view, so
  they are annotated with `// phpcs:ignore` and a rationale.

phpcs ForbiddenFunctions now reports 0 findings. 721 tests green.
2026-09-13 20:20:25 +03:00
Divarion_D cb4d12d99b fix(process): accept null pid in ProcessManager checks (CLI monitor crash)
The cs-fix auto-typing narrowed the pid parameters of the /proc-based
process checks to a non-nullable `int` (read from the `@param int $pid`
docblocks). But every one of these methods was written to tolerate a
missing pid: each opens with `$pid = (int) $pid;` and a `$pid <= 0`
guard, and their callers routinely pass nullable DB columns
(`tv_archive_pid`, `monitor_pid`, `vframes_pid`, `streams.pid`, …) and
runtime PIDs that can be null. In production this crashed the monitor
cron:

  ProcessManager::isNamedProcessRunning(): Argument #1 ($pid) must be of
  type int, null given, called in Cli/Commands/MonitorCommand.php:322

Make the pid parameter nullable (`?int`) on the eight affected public
checks — isRunning, isNamedProcessRunning, isStreamRunning,
producerKind, resourceSample, kill, getProcessAge, isStreamAlive,
isMonitorAlive — so the existing `(int) null === 0` cast and guard
return the safe "not running" result instead of a fatal TypeError. The
docblocks are corrected to `int|null` as well so a future cs-fix run
does not re-narrow the types. procExists stays `int` (internal, only
reached after the guard). No behaviour change for valid pids.
2026-09-13 19:57:38 +03:00
Divarion_D e8483a7345 fix(security): restore access-control guards mangled by Rector's De Morgan
The stage-2 pass ran SimplifyDeMorganBinaryRector, which — negating a condition
whose operand is an assignment — dropped the grouping parens:

    // was (correct):
    if (!(($rLine = UserRepository::getLineById($rID)) && Authorization::check('line', $rID)))
    // Rector produced (broken):
    if (!$rLine = UserRepository::getLineById($rID) || !Authorization::check('line', $rID))

By PHP precedence the second form does NOT mean `!((r=...) && check())`: it
returns "not failed" when the entity is missing OR the caller is unauthorised,
and leaves $rLine holding a bool. That is a broken-access-control bypass — a
background security review flagged it in ResellerAPIWrapper.

Scope: 49 conditions were mangled — 5 in ResellerAPIWrapper, 44 in
AdminAPIWrapper — most guarding Authorization::check / isset on get* lookups.
All were correct (parenthesised) on main; only the unpushed refactor/rector
branch was affected. Restored each to `!($x = f()) || !cond` (De Morgan of the
original). The `strtotime`/StreamView `!$x = f()` sites (no trailing `||`) parse
correctly and were left as-is.

Also disable SimplifyDeMorganBinaryRector in build/rector.php (its parens-drop
on assignment-in-condition outweighs the cosmetic wins) and document both
parens-bug variants + review greps in the config header.

Verified: both bug-pattern greps return nothing; PHPStan level 5 clean; suite
721 tests / 0 errors; Rector at fixpoint.
2026-09-13 19:37:08 +03:00
Divarion_D f38e4a411c refactor: Rector convergence pass — De Morgan / simplification (reviewed)
A small follow-up pass (3 files) picking up second-order simplifications exposed
by the stage-2 changes. All behaviour-preserving:
- PortalHandler: De Morgan on the set_claim / set_hdmi_reaction guards
  (!(empty(a) || empty(b)) -> !empty(a) && !empty(b)).
- Player\ListingsController: empty-if/else inversion (0 >= n {} else {} -> 0 < n {}).
- Reseller\TableController: strlen(x) > 0 -> (string) x !== ''.

No dropped-parens bug. PHPStan level 5 clean; suite 721 tests / 0 errors.
2026-09-13 19:27:53 +03:00
Divarion_D 9665a5a2ab refactor: expand Rector to Streaming / Public\Controllers / Ministra (stage 2)
Widen the Rector config to the remaining class-based trees (Streaming,
Public\Controllers, Ministra), keeping the templates, procedural
front-controllers (Public/stream, Public/admin, Ministra/portal.php) and the
streaming hot-path bootstraps out. Also document the recurring dropped-parens
bug + the review grep in the config header.

The resulting 107-file pass (94 Public\Controllers, 11 Streaming, 2 Ministra)
was reviewed against the suite + PHPStan + the usual scans:
- No dropped-parens bug this time (the assignment-in-condition pattern didn't
  occur in these files).
- 6 locally-called private static helpers became instance methods
  (behaviour-preserving; all called via $this->, no static call sites); one
  unused private param dropped (FanoutConfig::desired $rSnapshot, call site
  updated).
- De Morgan / ternary / dead-code simplifications; two `$x = getById()`
  truthy-assignments folded into the condition (no comparison, safe).

PHPStan level 5 clean; suite 721 tests / 0 errors.
2026-09-13 19:21:44 +03:00
Divarion_D b9546dc2c8 refactor(radio): third Rector batch (reviewed) + decompose massEdit
Reviewed Rector batch (3 files) + a manual decomposition of massEdit on top.

Rector batch (reviewed against suite + PHPStan):
- FIX (recurring Rector bug): the boolean inversion again dropped the parens on
  an assignment-in-condition in massEdit (array_search category DEL) — restored.
- massEdit: many correct empty-if/else -> guard inversions (verified).
- strlen(x) > 0 -> x !== '' in saveStreamOptions, TMDbService::buildUrl and
  ResellerTableRenderer (behaviour-preserving).

massEdit decomposition (166 -> 96 lines, nesting ~10 -> 7), test-first:
- computeCategoryChange()  — ADD union / DEL remove / SET replace (pure)
- planBouquetChanges()     — SET/ADD/DEL bouquet attach/detach plan (pure)
- planServerTreeForStream()— per-stream streams_servers reconcile (update in
                             place / batch-insert buffer / delete marking)
- raiseTimeLimits()        — the set_time_limit + ini_set block, shared with
                             massDelete (de-duplicated)

Tests: RadioServiceTest gains computeCategoryChange (5) + planBouquetChanges (3);
new RadioServiceMassEditTest characterizes massEdit end to end (invalid input,
and the server-tree ADD path) — it has no verifyPostTable/exit and
StreamProcess::updateStreams is a no-op with caching off, so the whole method
runs against the SQLite harness. That test guarded the server_tree extraction.

PHPStan level 5 clean; suite 721 tests / 0 errors.
2026-09-13 18:57:54 +03:00
Divarion_D 7aa7b0d29d refactor(radio): decompose RadioService::process into tested helpers + flatten
process() was a 250-line, ~10-deep method riddled with the empty-if/else
anti-pattern — the exact shape Rector mis-transformed (which is why RadioService
was reverted from the Rector pass). Rebuild it safely, test-first:

Extracted 7 helpers, each covered by tests (RadioServiceTest, 18 tests):
- buildAutoRestart()        — auto_restart schedule from days/time
- resolveSelectedIds()      — created-name or numeric id (merged the duplicated
                              bouquet/category resolution)
- createMissingBouquets()   — insert bouquets from bouquet_create_list
- createMissingCategories() — insert radio categories from category_create_list
- saveStreamOptions()       — clear + re-insert streams_options (6 option types)
- syncServerTree()          — reconcile streams_servers against the server tree
- syncBouquets()            — attach to selected bouquets, detach on edit

Flattened the orchestrator: 250 -> 87 lines, nesting ~10 -> 4. Guard clauses for
validate / auth / no-source, ternaries for the flag fields, and the vestigial
single-element $rImportStreams loop removed (mutating $rArray directly is
equivalent to its per-iteration merge). exit() on auth failure is unchanged
(returning instead would be a semantic change). Behaviour preserved throughout.

RadioServiceProcessTest characterizes the outer guard flow (INVALID_INPUT, and
NO_SOURCES via the edit path) — the branches the flattening restructures. The
success path can't run under the SQLite harness (verifyPostTable hits MySQL
information_schema) so its internals are covered by the extracted-helper tests.

PHPStan level 5 clean; suite 711 tests / 0 errors (+20).
2026-09-13 18:33:02 +03:00
Divarion_D 45488a4289 refactor: second Rector pass (reviewed) — more dead-code / simplification
Another `make rector-fix` pass over the same trees converged further: dead-code
removal and empty-if/else collapses across 36 files (CLI commands/cronjobs, VOD
and stream services, bootstraps). Reviewed against the full suite + PHPStan + a
targeted scan; committed as the corrected result.

- FIX (Rector bug, recurring): the boolean inversion again dropped the parens
  around an assignment-in-condition — restored in GroupService
  (removeGroupsFromUsers). It also hit RadioService, which was REVERTED wholesale
  (see below), so no fix is carried for it.
- REVERT: src/Domain/Stream/RadioService.php is left unchanged. Rector both
  introduced the dropped-parens bug there and altered control flow so a
  previously-reachable `return null` became unreachable — a behaviour change in
  an untested method that isn't worth verifying by hand.
- VERIFIED SAFE: RemoveUnusedPrivateMethodParameterRector dropped genuinely
  unused params — TmdbCron::processSeries (an unused by-ref &$updateSeries, not
  the one processEpisode uses) and ResellerTableRenderer::handleActiveCodes
  ($rIsAPI/$rPermissions); both sole call sites were updated.

Verified: PHPStan level 5 clean (0), suite 691 tests / 0 errors.

NOTE: the inversion rule still deterministically reproduces the dropped-parens
bug — do not re-run `make rector-fix` without skipping it first.
2026-09-13 17:42:31 +03:00
Divarion_D dc7fa83b0a refactor: apply Rector safe sets + fix the transform's fallout (reviewed)
Ran `make rector-fix` (deadCode + codeQuality prepared sets) over the PSR-4
class trees (Core/Domain/Cli/Infrastructure). Net -600 lines: dead-code removal
and the empty-if/else collapse (the feedback_simplify_empty_else pattern this
adoption targeted). 99 files mechanically transformed.

The output was reviewed against the full test suite + PHPStan + a targeted scan;
this commit is the CORRECTED pass (no broken state in history):

- FIX (Rector bug): its boolean inversion dropped the parens around an
  assignment-in-condition — `if (($rKey = array_search(...)) === false)` became
  `if ($rKey = array_search(...) !== false)`, assigning the bool to $rKey and
  unsetting the wrong array offset. Restored parens in 7 sites (BouquetService,
  ChannelService, CategoryService). PHPStan caught only 1 of the 7; the rest
  were silent. Added BouquetServiceTest as a regression (proven to fail on the
  broken form).
- FIX (pre-existing, same class): GroupService::removeGroupFromUsers had the
  identical dropped-parens bug already in the tree — corrected here too.
- FIX: ServersCronJob::pingServer returns floor() (float) under an `: int`
  return type — added an (int) cast.
- ACCEPT: LocallyCalledStaticMethodToNonStaticRector converted 25 locally-called
  private static helpers to instance methods (behaviour-preserving; call sites
  rewritten). Updated MonitorCommandTest's reflection helper to invoke on a
  constructor-less instance.

Verified: PHPStan level 5 clean (0), suite 691 tests / 0 errors.

NOTE: do not re-run `make rector-fix` on this tree without first skipping the
inversion rule — the dropped-parens bug is deterministic and would return.
2026-09-13 17:27:42 +03:00
Divarion_D 98aea670c3 build(rector): add Rector scaffolding (stage 1 — config + make targets + docs)
Adopt Rector as a require-dev tool alongside PHPStan/phpcs for safe, mechanical
refactoring:

- src/composer.json: add rector/rector ^2.0 (require-dev) + refactor/refactor:dry
  composer scripts.
- build/rector.php: narrowly-scoped config over the PSR-4 class trees
  (Core/Domain/Cli/Infrastructure). Skips the \TMDB lib, the streaming hot-path,
  vendor, tmp/backups. Import-adding stays OFF (the check-procedural-use gate
  relies on positional use imports). Behaviour-changing rules are disabled
  (SafeDeclareStrictTypes, UseIdenticalOverEqualWithSameType); only the safe
  deadCode + codeQuality prepared sets run (incl. the empty-if/else collapse).
- Makefile: `make rector` (dry-run, non-zero on pending changes) and
  `make rector-fix` (apply). Both require dev-tools.
- docs/en/guides/refactoring.md + dev-workflow.md + mkdocs.yml nav: the
  detect -> diff -> verify -> apply workflow.

No source code is changed by this commit — scaffolding only. `make rector-fix`
output will be reviewed separately.
2026-09-13 16:48:17 +03:00
Divarion_D e5798ae951 chore(dead-code): comment out unused ImageUtils::resize/generateThumbnail + add index
Both methods have zero call sites anywhere under src/ (grep, excluding vendor).
Comment them out rather than delete (kept as a record), and start a dead-code
index (DEAD_CODE.md) tracking such symbols per file with the verification
command.

Also resolves the last PHPStan level-5 error: generateThumbnail contained an
unreachable 32x64 branch (type 5 is matched earlier); the whole method is dead
regardless, so commenting it out clears the finding. PHPStan is now clean.
2026-09-13 16:43:35 +03:00
Divarion_D e23d4a66a8 fix(types): correct over-narrowed parameter types from the cs-fix auto-typing
The cs-fix run in 758a9cab auto-inserted native parameter types from docblocks
(the SlevomatCodingStandard.TypeHints.ParameterTypeHint sniff is a fixer). Some
docblocks were inaccurate, so the inserted types were stricter than the values
the code actually receives — PHPStan (level 5) flagged the fallout. Fixes:

Over-narrowed types (would fatal at runtime, restored to real contract):
- DropboxClient GetFiles/Copy/Move: string -> string|object on the path params
  (the methods handle a metadata object via is_object(), like DownloadFile/
  Restore/GetCopyRef); GetMetadata $rev: null -> ?string.
- Bouquet addItems/removeItems $rIDs: array -> array|int|string (callers pass a
  single scalar id; the is_array() guard wraps it).
- EpgService searchRecursive $rArray: array -> mixed (it recurses over every
  value, scalars included; the is_array() guard skips non-arrays).
- Encryption generateUniqueCode $pass: string -> ?string (callers pass
  SettingsManager::get('live_streaming_pass'), which can be null).

Correct types, dead guard removed (behaviour unchanged):
- Request::parseIncomingRecursively, StreamProcess::isLocallyMountedPath,
  FfmpegPaths::binary, ArchiveCommand::isArchiveProcessForStream,
  player_utility_functions getUserStreams/getUserSeries.

PHPStan: 15 -> 1 (the remaining one is a pre-existing ImageUtils logic bug,
unrelated to typing). Suite: 685 tests, 0 errors.
2026-09-13 16:31:14 +03:00
Divarion_D a788a450a5 fix(server): accept null permissions in getStreamingSimple/getProxySimple
Admin-API boot runs ResellerAPI::init() on the login page (pre-auth), where the
$rPermissions global is still null. It passes that null straight into
ServerRepository::getStreamingSimple()/getProxySimple(), whose strict
`array $rPermissions` hint turned it into a fatal TypeError at boot:

  ServerRepository::getStreamingSimple(): Argument #1 ($rPermissions) must be of
  type array, null given, called in .../ResellerAPI.php on line 72

Both methods only read $rPermissions via isset($rPermissions['is_reseller']), so
null is functionally equivalent to "no reseller restriction". Make the param
`?array $rPermissions = null` on both (restoring pre-typing tolerance) rather
than patching all ~28 call sites that feed the global. The strict `array` type
stays on every other repository method that runs post-auth with a real array.

Add ServerRepositorySimpleTest as a regression (null perms, online filter,
reseller name masking, proxy list) against the SQLite TestDb.
2026-09-13 16:10:40 +03:00
Divarion_D 284798ede7 fix(db): let Database accept a null host again (CLI boot crash)
The panel boots the DB with `new DatabaseHandler()` and no arguments, so $host
is null and the real credentials are resolved by the bundled XC_VM extension in
db_connect() — dbhost is never used on that path. A strict
`normalizeHost(string $rHost)` hint turned that normal null into a fatal
TypeError at bootstrap (initDatabase → __construct → normalizeHost), taking down
console.php / cron boot:

  Database::normalizeHost(): Argument #1 ($rHost) must be of type string,
  null given, called in .../Database.php on line 53

Make normalizeHost accept and pass through null (?string → ?string), restoring
the pre-typing behaviour; the explicit-credentials path (db_explicit_connect)
still passes a real string. Also make the constructor's implicitly-nullable
`string $x = null` params explicitly `?string` (same root cause, no behaviour
change, and avoids the PHP 8.4 implicit-nullable deprecation).

Add DatabaseHostTest as a regression (reflection on a constructor-less instance,
since the class needs the XC_VM extension to instantiate).
2026-09-13 16:05:56 +03:00
Divarion_D c08f2ba8ba style: drop the extra blank line after <?php in Core files
Collapse the double blank line between the opening <?php and the namespace
declaration to a single one across 17 Core files (Auth, Enum, Events,
Reference, bootstrap). Whitespace only — no code changes.
2026-09-13 15:20:38 +03:00
Divarion_D 6964e99f35 test(core): cover the DB layer — ModuleMigrator, SettingsRepository, AuthRepository
Fifth coverage batch, all driven against the in-memory SQLite TestDb:

- ModuleMigratorTest: install runs master database.sql (or falls back to
  replaying deltas <= target when there is none); up() applies only the
  (from, to] range ascending; uninstall runs database_drop.sql or no-ops; has()
  and discover() semver rules (non-semver files ignored, comments stripped);
  and failure propagation from a bad statement.
- SettingsRepositoryTest: getAll() JSON/CSV field normalisation (allow_countries,
  allowed_stb_types lowercase/trim/blank-drop, bouquet_name spaces, api_ips,
  shared_mount_prefixes incl. the legacy-CSV self-heal) and empty-list collapse.
  Points CACHE_TMP_PATH at a temp dir for the trailing FileCache write.
- AuthRepositoryTest: access-code and HMAC-key reads (getAllCodes with/without
  type filter, getCodeById, getCurrentCode via XC_CODE, getAllHMAC, getHMACById)
  and deleteHMAC existing/missing. getGroupPermissions (JSON_CONTAINS) and
  deleteCode (nginx config regen) are left for integration.

+20 tests. Suite: 678 tests, 0 errors.
2026-09-13 14:51:45 +03:00
Divarion_D c370dc47fc test(core): cover FileCache and the Translator backfill
Fourth coverage batch — file-backed cache and i18n, each driven against a
throwaway temp directory:

- FileCacheTest: set/get roundtrip through serialization, miss = false, has(),
  idempotent delete, flush, maxAge expiry (backdated mtime), and the path/age
  accessors.
- TranslatorTest: English default, cookie language detection with fallback,
  available() listing, {token} substitution, guarded setLanguage(), and the
  hardened backfill — a key missing from the active language is written back
  with the English value, or with itself as a visible placeholder when English
  has none either.

+15 tests. Suite: 661 tests, 0 errors.
2026-09-13 14:45:12 +03:00
Divarion_D a1228039fe test(core): cover Authorization checks and BruteforceGuard flood truncation
Third coverage batch — the pure/near-pure cores of the auth layer:

- AuthorizationTest: hasResellerPermissions() flag lookup; check() short-circuit
  when identity globals are absent; the 'user'/'line' report-tree scoping via a
  real SQLite query (owner/member within self + all_reports); and the 'adv'
  permission logic (admin required, super-admin group 1 bypasses the advanced
  list, other groups gated by it).
- BruteforceGuardTest: truncateAttempts() drops entries older than the window in
  both indexed (reindexed) and associative (keys preserved) shapes; all-recent
  and empty inputs. The IO-bound check* methods are left for integration tests.

+12 tests. Suite: 646 tests, 0 errors.
2026-09-13 14:42:08 +03:00
Divarion_D d3555ebb20 test(core): cover SettingsManager, DomainResolver, RequestManager, XML streamer
Second coverage batch — config, request state and the XML parser:

- SettingsManagerTest: set/getAll roundtrip, update, and the typed getters.
  Locks the deliberate split on a present-null value (get()/getArray() fall back
  via ?? while getBool/getInt/getString cast), and getBool()'s PHP truthiness.
- DomainResolverTest: the non-proxied resolve() path — forced vs kept protocol,
  https on :443, Host-header port stripping, and the domain_name / server_ip
  fallbacks. Captures a latent bug: an https:// prefix in domain_name yields
  'https:host' because '/' is stripped before 'https://' in the str_replace
  order (documented in the test so a fix updates it deliberately).
- RequestManagerTest: same store shape as SettingsManager, with has() on isset
  semantics (present-null reads as absent).
- XmlStringStreamerTest: end-to-end StringWalker (depth-2 children) and
  UniqueNode (named element) parsing over an in-memory stream, empty document,
  the File "missing path" and UniqueNode "missing option" error paths.

+26 tests. Suite: 634 tests, 0 errors.
2026-09-13 14:39:43 +03:00
Divarion_D 6d3cf2b25e test(core): cover the Http\Request input abstraction and sanitizers
Request had no tests despite being the front door for all HTTP input. Built
from injected arrays (no superglobals), the new suite locks:

- GET/POST merge precedence (POST wins), input/get/post/has/all accessors and
  their defaults.
- Typed accessors: getInt() coercion, getBool() via FILTER_VALIDATE_BOOLEAN.
- Server-derived helpers: method/isPost/isAjax/uri/userAgent/host (with the
  HTTP_HOST -> SERVER_NAME fallback) and their empty-server defaults.
- Client IP resolution: first valid X-Forwarded-For entry, skipping invalid
  headers to X-Real-IP, and the 0.0.0.0 sentinel when nothing is usable.
- Security sanitizers (the important part): NUL-byte stripping, ../ traversal
  neutralisation, <script>/<!-- --> defusing, stripslashes, CRLF normalisation,
  key scrubbing (__x__ / double-dot / htmlspecialchars), and recursive in-place
  cleanGlobals().

+13 tests, 57 assertions. Suite: 608 tests, 0 errors.
2026-09-13 14:29:54 +03:00
Divarion_D 81b66809dd test(core): cover UpdateChannels and the module extension registries
First batch of the src/Core coverage effort — pure in-memory / settings-driven
classes that had zero tests:

- UpdateChannelsTest: per-repo channel resolution (main/bin/fanout read their
  own update_channel_* setting), the 'unstable'->'beta' legacy alias, the
  unknown/empty -> 'stable' fallback, and forRepo() dispatch (BIN/FANOUT map to
  their channel; MAIN/UPDATE/PROXY/unknown follow MAIN). Seeded via
  SettingsManager::set().

- ModuleRegistriesTest: TableRegistry (register/lookup/last-wins), TopbarRegistry
  (order sort, per-label last-wins, export-page + log-type flags), QuickToolsRegistry
  (group rows, keys, handler lookup) and PermissionRegistry (ordered deduped set).

+15 tests, 41 assertions. Suite: 595 tests, 0 errors.
2026-09-13 14:26:46 +03:00
Divarion_D c1dd191d1b test: repair the SQLite test harness broken by strict typing
Two regressions surfaced when running the suite (9 + 3 errors):

- setDb(DatabaseHandler) rejects the SQLite double: the DatabaseAware
  refactor gave setDb() a strict DatabaseHandler hint, but TestDb was a
  standalone class. Make TestDb extend DatabaseHandler (a real subtype) so
  it satisfies the seam; its own constructor wires sqlite::memory: and never
  calls the MySQL-connecting parent constructor. Overridden methods widen
  parameter types (contravariant) and add return types, so LSP holds.

- validateHMAC(int|string $rExpiry) rejects null: HmacTokenTest passed null
  for an intentionally-empty expiry. Pass '' — identical HMAC input, and the
  strict type stays correct (the sole production caller always passes a value).

Suite is green again: 580 tests, 0 errors.
2026-09-13 14:22:55 +03:00
Divarion_D 70dadf0420 test: silence stdout/error_log noise during the test run
Exercised code paths echo diagnostics (dropOrphans() dropped-viewer lines,
GeoLite2 "[ERROR]" messages, ModuleLoader/GitHubReleases error_log output)
that clutter PHPUnit output without any test asserting on them:

- FanoutSyncOrphanTest / GeoLiteReleaseUpdaterTest: wrap each test in
  ob_start()/ob_end_clean() via setUp()/tearDown().
- tests/bootstrap.php: route error_log() to /dev/null.

No production behaviour changes.
2026-09-13 14:16:29 +03:00
Divarion_D 758a9cab0b style: apply K&R + tab formatting across the codebase (make cs-fix)
Mechanical, behaviour-preserving reformat produced by 'make cs-fix' under
the new build/phpcs.xml.dist ruleset: K&R braces, tab indentation, and the
other whitespace normalisations. No logic changes.
2026-09-13 14:14:23 +03:00
Divarion_D 5843f18bb7 build(cs): adopt K&R + tab house style, require param type hints
Rework build/phpcs.xml.dist from strict PSR-12 to the project's actual
house style so 'make cs-fix' is idempotent against the codebase:

- K&R (one-true-brace) instead of Allman; enforce via
  Generic.Classes.OpeningBraceSameLine +
  Generic.Functions.OpeningFunctionBraceKernighanRitchie, and exclude the
  PSR12/PEAR/Squiz messages that push the opening brace to a new line.
- Tab indentation instead of 4 spaces: DisallowSpaceIndent + ScopeIndent
  (tabIndent), and disable the tab-incompatible alignment sniffs
  (MultiLineCondition, FunctionCallSignature, ControlStructureSpacing) plus
  ConcatenationSpacing newlines so phpcbf converges (0 FAILED TO FIX).
- Restrict to PHP only (extensions=php) so .js/.css are never touched.
- Drop Generic.Formatting.SpaceAfterNot and Generic.PHP.RequireStrictTypes
  (the codebase does not use declare(strict_types)).
- Add SlevomatCodingStandard.TypeHints.ParameterTypeHint to catch
  untyped parameters.

Update CONTRIBUTING.md to run 'make cs-fix' first and describe the style.
2026-09-13 14:14:13 +03:00
DanilandGitHub 0bde979972 Merge pull request #188 from obscuremind/main
Sync Repository
2026-09-13 12:37:24 +03:00
Divarion_D 71ccdac452 refactor: convert inline \XcVm\ FQNs to PSR-4 use imports
Replace fully-qualified \XcVm\... class references (in code and in
docblocks/@see/@param/@return/@throws) with short names backed by
top-of-file use imports, project-wide. Same-namespace references drop
the prefix with no import; view templates gain the top-level imports the
check-procedural-use gate expects. Purely mechanical, no behavior change.
2026-09-13 12:13:01 +03:00
Divarion_D 3ae64f6184 refactor(db): resolve DB via DatabaseAware self::db() instead of threading global $db
Remove the "global $db passed as a function parameter" pattern: these
classes now `use DatabaseAware` and call self::db() (same DatabaseFactory
connection), dropping the $db parameter from every handler and its call
sites. ResellerTableRenderer/Controller also stop threading $rSettings
(use SettingsManager). Callers of DiagnosticsService::download/submitPanelLogs
drop the now-dead DatabaseFactory::get() argument. Inline \XcVm\ refs in
these files converted to PSR-4 use imports along the way.
2026-09-13 12:12:34 +03:00
Divarion_D 07a60eb219 refactor(core): typing + Router/Translator/parsing cleanups
- File, GitHubReleases: add PHP 8.1 property/param type declarations.
- Router: drop unused hasRoute/hasApiRoute/getRoutes, add param/return
  types, and fix denyAccess() — it guarded on a non-existent global
  goHome(), so it never redirected; call AdminHelpers::goHome() instead.
- Translator: cache en.ini per request, make the missing-key backfill
  atomic (check-then-append under LOCK_EX), sync current language on
  en.ini fallback, harden glob()/strict in_array.
2026-09-13 12:12:34 +03:00
Divarion_D 173a7e27af feat(profile): expose design (customizer) options in edit profile
Add an Appearance section (primary color, theme, skin, semi-dark, layout/navbar/header/content/rtl) to the admin profile, prefilled from the effective UI prefs (XC_VM_UIEffective) and persisted via the existing save_ui_prefs endpoint. Makes the desktop-only template customizer settings reachable on mobile, where the customizer panel is hidden.
2026-09-12 16:57:50 +03:00
Divarion_D 191c78ff31 feat(settings): add per-panel Responsive Tables toggle
New 'Responsive Tables' switch (Settings -> General -> Preferences): on = DataTables collapse columns into an expandable row on narrow screens (default), off = full-width tables with a horizontal scrollbar. Applies to admin and reseller. Stored inverted in settings.disable_table_responsive (migration 019 + database.sql); both footers wrap $.fn.DataTable to force responsive:false and hide the control column when disabled.
2026-09-12 16:57:49 +03:00
Divarion_D 1cfd65d3c3 feat(bouquets): add move up/down/top/bottom buttons to the sort editor
Drag-and-drop was the only way to reorder channels after the Bootstrap 5 migration, which is painful for large bouquets. Restore per-row up/down arrows and add move-to-top/bottom, alongside the existing drag-and-drop; order is still read from the list on save, so no backend change.
2026-09-12 16:57:49 +03:00
Divarion_D 68630027cd fix(streams): apply the channel filter on the live connections page
The admin live-connections view never seeded #filter-stream/#filter-user from the stream_id/user_id/server_id query params, so clicking a channel's connections count showed all live connections instead of that channel's. Seed the filters from the request (mirroring the reseller view) so the deep link filters as expected.
2026-09-12 16:57:48 +03:00
Divarion_D bf2c477de1 i18n(en): add keys for bouquet sort, responsive tables and profile appearance 2026-09-12 16:57:48 +03:00
Divarion_D 11b1adfd19 style(active-codes): sort ActiveCodeDetailsController import alphabetically
Fixes the phpcs SlevomatCodingStandard AlphabeticallySortedUses violation in
src/Public/routes/admin.php.
2026-09-12 13:37:28 +03:00
Divarion_D 468e70a1f8 refactor(active-codes): unify tables to standard DataTable and localize all pages
- Rebuild the batch managers (admin + reseller) on the standard client-side
  DataTable template; align the codes tables' toolbar, responsive control column
  and action buttons with the other admin tables (reseller order array updated).
- Localize every user-facing string across the 8 Active Codes views via
  $language::get(); add the missing keys to en.ini (navbar labels included, which
  previously rendered raw keys).
- Fix clipboard copy over HTTP (temp textarea inside the open modal + explicit
  selection) and the mass-edit "no matching codes" (read row.id from the keyed
  payload); replace alert()/confirm() with xcToast()/xcConfirm().
2026-09-12 13:26:07 +03:00
Divarion_D 0f5b15c9fd refactor(active-codes): render the voucher-details modal server-side
Move the voucher-details modal body out of a JS template string into a
server-rendered view (Views/admin/active_code_details.php), served as a raw HTML
fragment by a dedicated ActiveCodeDetailsController; the admin ajax controller
keeps only its JSON endpoints. The reseller list reuses the same fragment via
ResellerApiDispatcher. Portal / M3U base URLs are resolved from the request
origin, fixing the malformed "://host:" links.
2026-09-12 13:25:56 +03:00
Divarion_D 1f321dfab2 fix(active-codes): correct package/bouquet lists, null-safety and reseller portal URL
- Admin generate/list/mass controllers showed no packages: the group filter was
  hardcoded to 1; use getAll(null, 'line') so admins see all line packages.
- Reseller bouquet grid rendered empty tiles: BouquetService::getAll() returns
  reshaped rows without id/bouquet_name; use getAllSimple().
- Guard ActiveCodeService list methods (getBatchSummary, getResellersWithCodes,
  getRecentBatchNames, getResellersForAssignment) with ?: [] so a failed query
  degrades to an empty list instead of a TypeError.
- Reseller voucher-details built a malformed portal/M3U base ("://host:") from
  DomainResolver; resolve it from the request origin instead.
2026-09-12 13:11:10 +03:00
Divarion_D dbdab9b050 Prepare release 2.5.1 2026-09-11 20:12:09 +03:00
Divarion_D 3757bd97a9 fix(logs): stamp the panel version per error, at the moment it occurred
Errors were attributed to the panel's current version at send/parse time, so a
log flushed by cron right after an update tagged old-version errors with the new
version. Freeze the version where the error happens and carry it end-to-end.

- Logger::log() records `version` (XC_VM_VERSION) in each error_log.log entry
- panel_logs gains a `version` column (migration 020 + database.sql)
- ErrorsCronJob carries the per-error version into panel_logs
- DiagnosticsService sends and downloads the per-error version (not one batch
  version for the whole report)

Legacy records without the field fall back to empty/NULL, and the log server
falls back to the batch version for panels that don't send a per-error one.
2026-09-11 19:53:13 +03:00
DanilandGitHub 7264767cc6 Merge pull request #187 from obscuremind/main
fix(streaming): daemon-only delivery gaps — delay, kicks, encrypted HLS, catch-up, LLOD
2026-09-11 19:16:31 +03:00
Divarion_D b8325fc19e feat(active-codes): Smart Activation Codes system
Pre-generated stock voucher codes that pair with an auto-created line and
count down only on the client's first activation. Full admin + reseller
management — list, generate wizard, batch manager, mass edit — plus an
activation portal and a public activation API.

- schema: activation_codes table (migration 019 + database.sql)
- domain: ActiveCodeService (generate / activate / mass actions / export /
  batch summary; view queries live here, not in the templates)
- admin + reseller controllers and views; PortalController + activation API
- nginx routes for /active_code.php and the active_code API endpoint
- wired into auth, users, reseller dispatcher, table/player APIs and navbar

Passes phpstan, phpcs, make gates, and the unit suite (524 tests).
2026-09-11 19:08:20 +03:00
Divarion_D ddbc58c358 Merge branch 'main' of https://github.com/Vateron-Media/XC_VM 2026-09-11 15:50:30 +03:00
Divarion_D 69027c18fe refactor(admin/settings): move On Demand Probesize to the Streaming tab
Group the three probe fields (Analysis Duration, Probe Size, On Demand
Probesize) in one row on the Streaming tab with matching input widths. On
Demand Probesize previously sat alone on the General tab with a wider input.
No behaviour change: same field id/name, still saved by the settings form.
2026-09-11 15:50:21 +03:00
Divarion_D b461e71cc1 feat(test-stream-generator): shared TS encoder, keyframe-aligned joins, launcher
Serve /stream.ts from one always-running encoder fanned out to every client
(TsBroadcaster) instead of spawning a fresh ffmpeg per connection: the process
count stays fixed at two (one HLS, one TS) no matter how many clients or
repeated on-demand pulls connect, and a joiner attaches to the live byte stream
with a keyframe-aligned prebuffer, so opening the channel is instant.

Emit TS keyframes every 0.4s so a consumer that joins mid-GOP (e.g. the panel's
LLOD probe with analyzeduration=0.5s) finds an IDR + SPS/PPS within its analysis
window — otherwise it reports "dimensions not set" and never starts.

Add run-background.sh (start/stop/status/restart, PID + log, auto-picks the
bundled XC_VM ffmpeg build that actually runs) and an M3u/ sample playlist;
refresh README to match.
2026-09-11 15:46:56 +03:00
DanilandGitHub 900dda00ec Merge pull request #186 from obscuremind/main
Inclusion of xc_vm fanout streamer and monitor
2026-09-11 15:07:23 +03:00
Divarion_D 34fb54973b fix(admin): drop duplicate per-page selector on mass delete/edit pages
The mass-delete and mass-edit views each render a custom "entries per page"
selector in the toolbar (#*_show_entries, wired to the DataTable's page
length), while DataTables' own length control was also rendered via
layout.topStart: 'pageLength' — a redundant, duplicate picker. Set
topStart: null so the toolbar selector is the single source of page size.
2026-09-10 19:43:03 +03:00
Divarion_D a242662fb6 fix(update): guard update/rollback against non-root invocation
The update/rollback flow shells out to `sudo` (the Python updater), which
only works when the process is already root — in XC_VM's model the xc_vm
user has no sudoers entry. Run as a non-root user, the nested sudo prompts
for a password it can never read (no tty) and the updater silently never
launches, leaving the server stranded at status=5 (updating) with nothing
left to reset it.

Add a private assertRunAsRoot() check at the top of the update and
rollback cases, before any status change, plus a launcher pre-flight
(updater script + interpreter present) so status=5 is only set once a
launch is actually possible.
2026-09-10 19:33:25 +03:00
Divarion_D 356c181652 Prepare release 2.5.0 2026-09-10 17:31:22 +03:00
DanilandGitHub 2eda47dd1d Merge pull request #183 from obscuremind/main
feat(fanout): expose the daemon's source backend as a panel setting
2026-09-10 16:26:36 +03:00
Divarion_D 6662e72798 refactor(geoip): generic GitHubReleases client + domain-owned release sync
Make the GitHub-release GeoIP updates cohesive and testable, and keep the
GitHubReleases class a generic release client.

- GitHubReleases: drop the domain-specific getGeolite()/getIspDatabase()/
  getAsnCatalog() and the private releaseAsset() — they hard-coded the
  maxmind paths and GeoIP filenames, which don't belong in a generic
  GitHub-releases client. Add the generic assetUrl($version, $asset)
  primitive; domain code now composes its own specs around it. Also drop a
  stale usage example that referenced the removed getGeolite().

- New XcVm\Core\GeoIP\GeoLiteReleaseUpdater owns the release-asset sync:
  builds the GeoLite2-City/Country and GeoIP2-ISP specs (url via assetUrl,
  md5 via getAssetHash, local maxmind path), downloads them through ONE
  path, and records the version into maxmind/version.json. This unifies
  GeoIP2-ISP with GeoLite2 — the ISP now gets the same dir self-heal,
  checksum-tolerant "always save" behaviour, status lines and chmod 0750
  (was 0640) instead of the leaner fetchReleaseFile() it used before.

- MaxMindCronJob is now a thin orchestrator (updateGeoLite/updateIsp);
  AsnCatalogSync builds its own blocked_asns spec via assetUrl/getAssetHash.

- Tests: GeoLiteReleaseUpdaterTest (specs/paths/version, error paths) via a
  mocked repo + protected download/record seams; GitHubReleasesTest covers
  assetUrl().
2026-09-10 16:11:29 +03:00
Divarion_D 5b35907388 refactor(admin): readable, testable stream entry points (+2 bug fixes)
Clean up the admin stream loopback scripts (api/live/proxy_api/thumb/
timeshift/vod) for readability and to give their core logic unit-test
coverage, without changing behavior — except the two bugs called out below.

Readability:
- Invert the pervasive empty `if (c) {} else { body }` into `if (!c) { body }`.
- Flatten deep if/else nests into guard-clause / elseif chains (live, vod).
- Drop dead blank lines.

Testability (pure logic extracted, then covered):
- New value object XcVm\Domain\Stream\AdminStreamToken (decode + isValid)
  replaces the uitoken decrypt+expiry+IP block duplicated across live/thumb/
  timeshift/vod; it reuses the existing-but-unadopted NetworkUtils::ipMatches().
  Token start/duration are kept raw (a timeshift date string must not be cast).
- Four pure helpers folded into the existing StreamUtils (not new one-method
  classes): sanitizeSegmentName, containerMimeType, timeshiftStartTimestamp,
  segmentRetryBudget — replacing inline path sanitization, a 44-line MIME
  switch, the timeshift date parser, and the retry-budget math.
- Tests: AdminStreamTokenTest, NetworkUtilsTest (ipMatches had no coverage),
  and StreamUtilsTest extended for the four helpers. OPENSSL_EXTRA is defined
  in tests/bootstrap.php so token round-trips work.

Bug fixes (behavior changes):
- live: the retry budget used `$rTotalFails < intval(...) ?: 20`, which by
  operator precedence parsed as `(... < ...) ?: 20` — always truthy — so the
  `seg_time * 2` floor was ignored and the budget was always the configured
  wait. Now max(seg_time*2, wait ?: 20) via StreamUtils::segmentRetryBudget().
- proxy_api: the per-second byte-rate divided by `time() - last`, which is 0
  for two samples in the same second (division by zero). Clamp to >= 1s.
2026-09-09 22:32:37 +03:00
Divarion_D 4556c4d54c docs: drop dts-audio-test refs, refresh test-generator blurb, reformat ffmpeg ADR
- CONTRIBUTING.md / tools/README.md: remove the deleted dts-audio-test row
  and update the test-stream-generator description (now generates a
  synthetic pattern, no input file).
- docs/adr/plan-ffmpeg-per-distro-distribution.ru.md: table/list whitespace
  reformat only.
2026-09-09 21:34:20 +03:00
Divarion_D ee3707f79a feat(tools): synthetic test pattern in test-stream-generator (no input file)
Drop the sample.mp4 dependency: generate the source live with ffmpeg
`testsrc2` plus overlays — a big running stopwatch (elapsed), the real
wall-clock (handy to eyeball end-to-end latency), a frame counter and two
sweeping boxes; 1 kHz test tone for audio. Falls back to `testsrc` (v1,
built-in timestamp) when no TrueType font is found.

New flags: --size, --fps, --font (autodetected). Removed -i/--input and
--encode (the raw synthetic source is always encoded).

Long-run hardening so it can sit generating for days:
- --max-clients caps concurrent /stream.ts pulls (each is an ffmpeg);
  extra connections get 503 instead of piling up processes/FDs.
- Per-client socket write timeout + TCP keepalive drops a stalled or
  half-open peer instead of pinning its ffmpeg; hard-kill if terminate()
  does not reap it.
- Per-request access logging is off by default (an HLS player polls every
  few seconds and would flood the log over days); --verbose restores it.
2026-09-09 21:33:06 +03:00
Divarion_D 347e257561 chore(tools): remove dts-audio-test helper scripts
Drop the one-off tools/dts-audio-test/ helpers (README + compare-audio.sh
+ gen-problem-stream.sh); no longer needed.
2026-09-09 21:08:57 +03:00
Divarion_D 25d9c1a80e fix(lb-install): add curl on ubuntu24/debian13 + rate-limit-proof tag resolution
LB provisioning on Ubuntu 24.04 (and Debian 13) failed with "Failed to
get latest binaries release tag" → no binaries → no php → install abort.
Two causes:

- The ubuntu24/debian13 package lists shipped libcurl but not the `curl`
  binary, so the node-side release-tag lookup (and the MD5 curl) had no
  curl. Every other distro list already included it. Added `curl` to both.
- The channel-aware tag lookup hit only the GitHub REST API, which is rate
  limited to 60/hour for unauthenticated callers and can be exhausted
  during a busy install. Added a non-API fallback resolved on MAIN via the
  github.com `/releases/latest` redirect (302 → …/releases/tag/<tag>),
  which is not rate limited, before the node-side lookup.

Resolution order is now: channel-aware API (cached) → github.com redirect
on MAIN → node-side releases/latest.
2026-09-09 21:08:22 +03:00
Divarion_D 036f1cea8d refactor(admin): replace native alert() with the xcToast notification
Route the admin views' error/validation notices through the shared
window.xcToast (Bootstrap 5 toast) instead of the native window.alert,
matching the panel's notification style. Types: 'error' for failures
(errText/errMsg/lang.error), 'warning' for input validation, 'info' for
progress, and success/error by condition for backup save and EPG import.

99 call sites across 46 admin view files; xcToast is defined globally in
footer.php and available on every admin page (and inside modals opened
over it).
2026-09-09 20:27:18 +03:00
Divarion_D 244dfaa191 fix(panel-logs): wire Panel Errors actions to the topbar
The Panel Errors page kept leftover card-header buttons whose ids
(btn-download-log, btn-clear-logs) collided with the new-UI topbar
buttons the shell already renders for this page. getElementById then
resolved the topbar nodes, leaving the card buttons dead: "Download
JSON" had no handler and "Clear Logs" hit the shared delegated handler
with no data-log-type and returned early.

- Remove the duplicate card action buttons and the redundant inline
  clear-logs modal/handlers; Clear Logs now uses the shared footer
  wiring (#btn-clear-logs + #xcClearLogsModal, data-log-type=panel_logs).
- Keep a single Download-JSON handler bound (guarded) to the topbar
  "Download log" button; use xcConfirm/xcToast instead of window.confirm.
- clear_logs: allow the ('adv','panel_logs') permission so a sub-admin
  with only the panel-logs grant can clear the panel error log.
2026-09-09 20:27:09 +03:00
Divarion_D eb252f82ca feat(fanout): track version in a sidecar file and probe integrity
Stop deriving the installed xc_fanout version from the binary's own
`-version` output. The version is now recorded in a sidecar file
(bin/xc_fanout/xc_fanout.version) and compared against GitHub, so a
locally-built or custom-signed test build is not force-overwritten just
because its self-reported version differs from the latest release — pin
it by writing the file to the release version.

Independently, probe the binary with `-version`: a binary that does not
answer is treated as missing/corrupt ("bit-rotted") and reinstalled
regardless of the recorded version. The sidecar is seeded from the
running binary on first run after upgrade so an up-to-date host is not
needlessly reinstalled, and written on every successful install. Also
switches the release channel to the per-repository FANOUT channel.
2026-09-09 20:09:50 +03:00
Divarion_D 7caff1513a feat(updates): per-repository update channels (stable/beta)
Replace the single global `update_channel` setting with independent
per-repository channels for MAIN (panel), BIN (compiled binaries) and
FANOUT (xc_fanout daemon). The GeoLite/ASN (UPDATE) and proxy (PROXY)
repos have no channel of their own and follow the MAIN channel.

- Add XcVm\Core\Updates\UpdateChannels resolver (main/bin/fanout/forRepo).
- Migration 016: add update_channel_main/bin/fanout, copy the previous
  selection into all three (unstable -> beta), drop update_channel.
- Settings UI: one "Updates" section with three channel selectors.
- Route every GitHubReleases call site through the resolver.
- LbInstallFlow: resolve the BIN tag channel-aware (was always /latest),
  so a BIN beta channel provisions LB nodes with pre-release binaries.
- XcvmCoreCommand: map the BIN channel to a git branch (stable -> main,
  beta -> beta) with a fallback to the stable branch when beta is absent.
2026-09-09 20:09:34 +03:00
DanilandGitHub 42e971933c Merge pull request #182 from Vateron-Media/fix/vod-analyzer-php8
fix(vod): stop cron:vod aborting on degenerate ffprobe output (PHP 8)
2026-09-08 20:41:22 +03:00
DanilandGitHub dbd8353a36 Merge pull request #180 from Vateron-Media/dependabot/composer/src/composer-22c089009e
deps: bump phpstan/phpstan from 2.2.9 to 2.2.12 in /src in the composer group
2026-09-08 20:40:31 +03:00
DanilandGitHub fde35439e3 Merge pull request #181 from Vateron-Media/feat/new-adminUI
Feat/new admin UI
2026-09-08 20:39:11 +03:00
Divarion_D 9fa0f91756 chore: remove settings_audit.md working notes
One-off settings-table audit doc (2026-08-30); a scratch artifact, not part of
the shipped docs.
2026-09-08 20:35:47 +03:00
Divarion_D 8ea1076702 style(core): sort ModuleLoader use imports alphabetically
phpcs (Slevomat AlphabeticallySortedUses) flagged the Permission/QuickTools
provider imports out of order. No behavioral change.
2026-09-08 20:33:50 +03:00
Divarion_D f1e43d8e0d fix(admin-ui): replace leftover mdi info icons in settings with Tabler
Two `mdi mdi-information-outline mr-1` inline info icons (MaxMind + token-secret
notes) were the last legacy-icon leftovers in a migrated core view → Tabler
`tabler-info-circle me-1`. Panel views are now icon-clean (only the excluded
third-party database.php stays legacy).
2026-09-08 20:20:20 +03:00
Divarion_D e5f02e19f8 revert(admin-ui): load the full iconify set again
Point both shells back at iconify-icons.css (full set) instead of the generated
subset, so module-introduced icons always resolve — the subset only scans core
source. The subset file and generator stay in the tree pending a decision. Also
drops the header-stats pill background per the tweak in the view.
2026-09-08 20:15:55 +03:00
Divarion_D 7ce620a864 docs(dev): document module provider contracts & controller/REST wiring
- module-extension-points: Topbar, Table, Permission and Quick Tools provider
  sections + the rule that a module owns its table end-to-end (clean JSON,
  module api routes, events — core never touches module tables).
- module-authoring: list the optional providers in the sub-contract tree and
  the method table.
- core-wiring: bootAll steps 6–9 (topbar/table/permission/quick-tools) and that
  registries run without a router.
- http-request-handling: REST path boots modules (registries only) and serves
  module tables generically via TableRegistry.
2026-09-08 20:15:54 +03:00
Divarion_D 492ba94ee4 refactor(core): delegate module concerns to providers; remove watch_output from core
Wire the new provider registries into the core consumers and stop hard-coding the
watch module's page in core:

- Topbar::config merges TopbarRegistry entries; the watch/plex page configs and
  their movies/series/settings injections are gone from core (modules declare
  them). Export/clear-logs gating now also honours the registry.
- TableController dispatches an unknown id to TableRegistry (default case) and
  filterRow() is public for module handlers; handleWatchOutput removed.
- PermissionReference::keys() merges PermissionRegistry; the four folder_watch*
  keys removed from the core list.
- quick_tools.php merges QuickToolsRegistry; post.php dispatches module tools;
  the clear_watch_logs tool removed from core.
- MovieService dispatches VodImportedEvent instead of UPDATE-ing watch_logs
  (also fixes a latent fatal when the module is uninstalled).
- watch_logs delete (MiscAjaxController) and clear (BackupAjaxController) removed;
  admin.php drops the watch_output route.
- AdminApiController REST serves any TableRegistry id generically; the REST entry
  point boots modules (registries only, no router) so module tables are reachable.
2026-09-08 20:15:42 +03:00
Divarion_D 9fd29aadcf feat(core): module provider contracts for topbar/table/permission/quick-tools
Add four opt-in module extension points (mirroring NavbarProvider), each with a
static registry populated in ModuleLoader::bootAll and a no-op BaseModule default:

- TopbarProviderInterface / TopbarRegistry — per-page action buttons, plus
  markExportPage()/setLogType() for a module's own log/report page.
- TableProviderInterface / TableRegistry — serverSide DataTable handlers keyed
  by table id.
- PermissionProviderInterface / PermissionRegistry — reseller sub-permission keys.
- QuickToolsProviderInterface / QuickToolsRegistry — one-shot Quick Tools (button
  + handler).

Also add VodImportedEvent so core can notify modules when a VOD item is imported
from a path instead of writing module-owned tables directly.
2026-09-08 20:15:28 +03:00
Divarion_D 10d36a9897 fix(vod): stop cron:vod aborting on degenerate ffprobe output (PHP 8)
The VOD analyzer processes to_analyze rows in a single pass and dies on the
first bad file, so on PHP 8 a single unplayable/placeholder movie leaves every
remaining episode stuck at `to_analyze = 1` (yellow) forever — the queue is
empty yet thousands of episodes never turn green/red.

Two PHP 7 -> 8 regressions in the VALID branch, both hit by files ffprobe can
open but returns degenerate metadata for (e.g. a truncated upload or an image
mislabelled as .mp4):

- `round(($rSize * 0.008) / $rSeconds)` threw DivisionByZeroError when the
  duration is 'N/A' (parsed seconds = 0).
- `$rFFProbee['codecs']['audio'|'video']['codec_name']` threw
  "Cannot access offset of type string on string": parseFFProbe() stores ''
  for a missing codec, which the branch dereferences as an array.

Fix: guard the bitrate division; treat a probe with no video stream as BROKEN
and skip it; normalise codecs.video/codecs.audio to arrays before use. The
analyzer now advances past bad rows instead of aborting the whole run.
2026-09-07 21:27:24 +03:00
Divarion_D 5b23a4807b docs(contributing): rewrite contributor guide 2026-09-07 21:00:01 +03:00
Divarion_D f08bc50e3d style(admin-ui): reformat admin view templates (formatter pass)
Whitespace/indentation-only reformat of the admin Bootstrap 5 view templates.
No behavioral change; all 107 files pass php -l.
2026-09-06 23:02:32 +03:00
Divarion_D 2cb902d0dd fix(admin-ui): episodes topbar — working Add button, single/multi swap, modal select2
- episodes: the topbar 'Add Episode' primary carries id=add-episode-btn so the
  existing handler opens the modal (with series pre-fill); removed the now
  duplicate in-card Add Episode button.
- episode: the topbar primary offers the opposite mode to the one open
  (Add Single on the Add Multiple page and vice versa) via a new rMulti ctx flag.
- Add Episode modal: the series select2 gets dropdownParent so its dropdown
  renders inside the modal instead of beneath it.
2026-09-06 22:58:40 +03:00
Divarion_D 4fa67bfe01 refactor(admin-ui): split vendored core.css into bootstrap.css + theme custom.css
core.css bundled Bootstrap 5.3.5 and the panel theme layer in one 828KB
file. Split at the clean rule boundary (after Bootstrap's .vr utility):
- bootstrap.css: vanilla Bootstrap 5.3.5 (188KB)
- custom.css: the theme layer / custom colors, components, overrides (621KB)
Both shells link them in the original order (bootstrap then theme) so the
cascade is unchanged. Removes the now-redundant core.css.
2026-09-06 22:48:41 +03:00
Divarion_D 6f8f091f1d build(admin-ui): add iconify subset generator script
tools/generate-iconify-subset.py scans the PHP/JS/CSS source for literal
tabler-* tokens and regenerates the used-only iconify-icons.min.css from the
full iconify-icons.css. Run it after adding a new icon. Also regenerates the
subset header banner to point at the script.
2026-09-06 22:48:40 +03:00
Divarion_D c47cc34a77 perf(admin-ui): ship a used-only iconify subset (2.6MB -> 55KB)
The full iconify-icons.css (2.6MB, 5928 icons) was loaded on every admin
and reseller page and parsed slowly. Only 148 tabler-* icons are actually
referenced in the source (all literal, no dynamic names), so generate a
minified subset containing just those and point both shells at it.

The full iconify-icons.css stays in the tree as the source to regenerate
from when new icons are added.
2026-09-06 22:29:11 +03:00
Divarion_D 5e1e3a1f36 fix(admin-ui): form UX — searchable owners, bouquet pickers on create, package defaults, edit-page menu highlight
- Owner pickers use Select2 with search: line.php + user.php (owner_id now
  a dropdown), and fix the mag/enigma remote owner search returning empty
  until a keystroke (always send search=, even blank). line.php also makes
  Forced Country searchable.
- bouquet.php shows the content-picker tabs on create too (backend already
  accepts items on add), and drops the now-unused $rShowTabs gate.
- package.php: Trial/Official are mutually exclusive, and a new package
  defaults to all Access Output formats enabled.
- menu.php: map edit/detail pages to their list page so the sidebar keeps
  the correct item highlighted (e.g. Edit Group -> Groups).
2026-09-06 22:17:05 +03:00
Divarion_D 975b3ad812 feat(admin-ui): stream tables — actions to end, topbar edit, filter deep-link, log refresh
- Move the Actions column to the end on the streams table and the
  stream_view Active Servers table.
- stream_view Edit/Manage buttons are generated by the Topbar provider
  (new stream_view config entry; topbar.php passes rID/rSID).
- streams page preselects #filter-status from ?filter= so the dashboard
  Live/Down Streams tiles land on a filtered view.
- Extend the topbar table-page list so log/list pages (credit_logs,
  user_logs, stream_errors, login_logs, mysql_syslog, mag_events,
  panel_logs, asns, backups) get the instant refresh + clear-filters.
2026-09-06 22:16:53 +03:00
Divarion_D ef5994b34d feat(admin-ui): fix navbar labels & missing i18n keys
- Rename the admin 'User Lines' items to Add Line / Manage Lines / Mass
  edit lines (they manage lines, matching the reseller panel).
- Drop the duplicate Live Connections entry from the Dashboard group
  (it stays under Logs).
- Add missing en.ini keys (mass_edit_lines, mass_edit_mags,
  mass_edit_enigmas, user_logs, generate_trial, no_information, restore,
  confirm_uninstall_module) so navbar/labels no longer echo raw keys.
2026-09-06 22:16:52 +03:00
Divarion_D 9afbb7b7b1 feat(admin-ui): style header live-stats as a separated pill
Group the header mini-stats into a rounded bg-body-tertiary pill with
vertical-rule separators and tinted icons (admin + reseller shells).
2026-09-06 22:16:38 +03:00
Divarion_D ed675fc1d3 style(admin-ui): CSS polish for dashboard cards, control column & modal close
- Dashboard per-server card styles (hover lift, stat icon chips, slim
  rounded metric bars).
- Responsive 'expand' control column rendered as a bold +/- with a
  'Details' header hint.
- Player/preview modal close (x) as a red danger disc with a white glyph.
2026-09-06 22:16:38 +03:00
Divarion_D 5c220fe486 feat(admin-ui): redesign dashboard server cards & fix sparkline tooltip
- Server cards: stat cells gain tinted icon chips + tabular values, CPU/
  MEM/IO/DISK rows show a live percentage next to slim rounded bars whose
  colour reflects the value, and the card lifts on hover.
- Sparkline: seed as {x: epoch-ms, y: cpu%} pairs (DashboardController) so
  the tooltip shows the real sample time and a named 'CPU' series instead
  of a bare point index.
- Also slows this dashboard's stat poll to 5s.
2026-09-06 22:16:27 +03:00
Divarion_D 79fc178f85 perf(admin-ui): reduce live-stat polling from 1s to 5s
Header mini-stats (admin + reseller) and the reseller dashboard polled
the stats API every second. Slow the interval to 5s to cut idle load
without a noticeable UX change.
2026-09-06 22:16:17 +03:00
Divarion_D 14fca8e5a0 refactor(assets): relocate remaining legacy assets, remove assets/admin/old
Complete the asset flattening: move the still-used legacy assets out of
assets/admin/old/ into the flat new-UI tree and delete old/ entirely.

- css/listings.css, js/listings.js, js/vendor.min.js → assets/{css,js}/
- images/countries, favicon.ico, logo-topbar.png, xcvm-login-bg.png → assets/img/
- libs/videojs → assets/vendor/libs/videojs
- Rewrite every remaining `assets/old/…` reference (EPG grid on admin+reseller
  epg_view/live_connections/line_activity, country flags across the log/IP
  tables, header logo, login favicon/background, player videojs) to the new
  paths, and login.css's background to ../img/.
- Delete assets/admin/old/ — the panel no longer references it.

Verified on canary: all relocated assets serve at the new paths (listings,
country flags, videojs, favicon, logo, login background), admin + reseller
pages render, the old paths now 404; gates green.
2026-09-06 19:33:24 +03:00
Divarion_D 06131ad46e refactor(assets): flatten new-UI assets to assets/admin, prune dead legacy
The redesign is complete, so the assets/admin/new/ nesting is no longer
needed. Promote it to the base directory (as the layout was originally) and
remove the dead legacy assets left behind by the old shells.

- Move assets/admin/new/{css,img,js,vendor,xcvm} up to assets/admin/ and
  rewrite every `assets/new/…` reference to `assets/…` (admin + reseller
  header/footer/vendors/login, the vendor-loader base path, data-assets-path,
  and login.css's background path). The reseller symlink (assets/reseller ->
  admin) is unaffected.
- Prune assets/admin/old/ down to what the migrated UI still uses: the EPG
  grid engine (css/listings.css, js/listings.js), js/vendor.min.js, the
  country flags (images/countries), favicon.ico, logo-topbar.png, the login
  background, and libs/videojs (player). Everything else (legacy Bootstrap 3
  theme css/js, old libs, unused images, fonts, empty videos) is removed —
  all verified to have zero references.

Verified on canary: admin + reseller pages render with the flattened asset
paths (core.css/custom.css/login.css/main.js all 200), EPG pages, country
flags, videojs and the login background still serve; gates green.
2026-09-06 19:24:41 +03:00
Divarion_D cf6a31871f chore(admin-ui): drop stale .newui.php references from docblocks
The admin/reseller shells were long ago renamed from *.newui.php to the base
filenames; only comments/docblocks still mentioned the old names. Comment-only
cleanup, no runtime change.
2026-09-06 19:14:56 +03:00
Divarion_D 7fb292fc2b refactor(reseller-ui): remove legacy reseller shell, promote new-UI to base
All reseller pages are migrated to the Bootstrap 5 shell, so the per-page
opt-in and the legacy chrome are dead code. Mirror the earlier admin-shell
cleanup:

- Drop xc_reseller_use_newui() and the $migratedReseller allowlist; the
  reseller branch of renderUnifiedLayoutHeader()/Footer() now always renders
  the Bootstrap 5 shell.
- Delete legacy layouts/reseller/{header,footer,modals}.php and the legacy
  per-page Views/reseller/topbar.php.
- Promote layouts/reseller/{header,footer}.newui.php to the base filenames.
- Refresh docblocks that referenced the removed function / .newui names.

Verified on canary: reseller login + all pages render the new-UI shell,
tables load, forms post; gates green.
2026-09-06 19:07:59 +03:00
Divarion_D abcd0578f3 feat(reseller-ui): migrate reseller panel to Bootstrap 5 new-UI
Bring the reseller panel to full parity with the migrated admin panel.
All 22 reseller pages now render inside the Bootstrap 5 shell.

Tables (lines, streams, movies, radios, users, mags, enigmas, episodes,
created_channels, live_connections, line_activity, user_logs) move off the
legacy positional-HTML renderer: ResellerTableRenderer now emits clean
keyed JSON per row (serverSide ordering/search/paging preserved; index-0
Responsive-control column with an `!empty($rOrder[...])` order guard), and
each view renders badges / status dots / action dropdowns client-side,
mirroring the admin views. Reseller data contract kept: `$rPermissions`
gating, ResellerApiDispatcher actions (connections/purge, line/mag/enigma
sub-actions, reg_user enable/disable/delete, adjust_credits, ticket
close/reopen), static owner/report-tree filters, and reseller-only bits
(Reset ISP Lock, WhatsApp renewal). No admin `multi`/ban/unban/fingerprint
where the reseller backend lacks them.

Views/forms (dashboard already done, plus epg_view, tickets, ticket,
ticket_view, edit_profile, and the line/user/mag/enigma package-driven
create/edit forms) rebuilt to new-UI with the POST contract preserved
byte-for-byte (ResellerPostController / ResellerAPI field names unchanged;
bouquets_selected[] array mechanism kept).

Each page added to the $migratedReseller allowlist in layouts/admin.php.
API-key (filterRow) branch and all non-migrated renderer paths untouched.
2026-09-06 19:02:21 +03:00
Divarion_D abaa7b0b6b feat(auth-ui): redesign admin & reseller login as sci-fi HUD
Replace the admin and reseller login pages with a single-look, dark
"Core Access" HUD console: a full-bleed cyberpunk backdrop with a
centred angular panel, red LED-style edge bars (glowing 45° corner
brackets + neutral edge runs), textured inputs with icon wells and
red accent bars, a chevron LOGIN button, and a subtle living animation
(phase-staggered glow breathing + a slow hue-drift shimmer), gated
behind prefers-reduced-motion.

- admin/login.php: rewritten to the HUD markup; PHP login contract
  preserved 1:1 (flood check, Authenticator, referrer/setup/dashboard
  redirects, reCAPTCHA, status messages). Intentionally not theme/hue
  aware.
- reseller/login.php: same HUD, adapted contract (STATUS_NOT_RESELLER,
  controller-provided $referrer). Reuses the admin login.css and bg via
  the Public/assets/reseller -> admin symlink.
- CSS extracted to assets/admin/new/xcvm/login.css (shared by both).
- Add xcvm-login-bg.png; drop the old login-bg.mp4 video.
- i18n: add admin_access / reseller_access.
2026-09-06 17:21:31 +03:00
Divarion_D c4208902f9 feat(admin-ui): rebuild stream editor EPG / Map / RTMP / Capture tabs
Replace the stream editor's deferred hidden passthrough inputs with real
Bootstrap 5 tabs: EPG (quick-search + XMLTV with epg_id->channel_id->lang
cascade from $rEPGJS, minute offset, and the "use EPG icon" picon modal),
Map (custom_map), RTMP push (rtmp_output switch + push-target table
collected into external_push), and Capture (capture_server_id). All field
names preserved; controller already supplied the data. Adds 4 i18n keys.
2026-09-06 14:34:57 +03:00