Commit Graph
116 Commits
Author SHA1 Message Date
obscuremindandGitHub 19607a9176 Update StreamProcessBuildLiveTest.php 2026-09-10 22:14:46 +01:00
obscuremindandClaude Opus 5 8e24a2427a feat(fanout): make encoder supervision a real, settable option
Production-readiness pass on the panel side of daemon encoder supervision.

Until now the feature was gated on a setting that did not exist as a
column, so it could only be enabled by hand-editing the database and the
daemon never learned about it at all.

* Migration 018 adds `fanout_supervise`, off for every existing install.
* Admin -> Settings gains the toggle, alongside the other fanout tuning.
* SettingsService saves it (it is a checkbox, so it has to be in the
  boolean list or it can never be turned back off).
* FanoutConfig writes it into the daemon's config file as `supervise`,
  which is how the node learns it may supervise at all. Both halves must
  be on for anything to change, and either one off is a full rollback.
* StreamProcess reads `fanout_supervise` rather than the placeholder
  name, matching the panel's `fanout_*` convention.

Also: the recorded command file now says WHO ran the stream. `_.fanout`
when the daemon owns the process (the bare command it was handed) and
`_.ffmpeg` when this node ran it itself (with the redirect-and-background
tail). Two names rather than one because the first question in any
incident is which path the stream took, and a single filename cannot
answer it; the stale one is removed so a stream that switched paths does
not leave a lie behind.

FanoutConfigTest covers the new key both ways, including that a settings
array predating it reads as off -- that absence is the upgrade path for
every existing install.

Verified: php -l clean on every changed file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 21:34:50 +01: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
rootandClaude Opus 5 553aed89ff feat(fanout): expose the daemon's source backend as a panel setting
xc_fanout 0.12.0 can convert a non-mp2t source to MPEG-TS in-process instead of
spawning an ffmpeg child per stream. For the common IPTV case — HLS whose
segments are already MPEG-TS — that child was doing little more than
concatenating bytes the daemon can concatenate itself, at ~27 MB RSS apiece,
plus a process spawn and a probe window on every on-demand join and reconnect.

The daemon reads which path to take from `source_backend` in its config.json;
this adds the panel setting that writes it.

  auto (default) — convert natively where the daemon's reader can, ffmpeg for
                   everything it declines
  ffmpeg         — always spawn ffmpeg; the pre-0.12 behaviour and the
                   kill-switch if a native pull ever misbehaves
  native         — native only, no fallback. Diagnostic: it answers "what is
                   actually eligible on this node", and a declined source there
                   is a dead channel rather than a slightly more expensive one.

auto is the default because the fallback makes it strictly safer than
ffmpeg-always: anything the native reader will not take (fMP4/CMAF HLS, RTMP,
SRT, RTSP, AES-128 encrypted sources, anything it cannot positively identify)
runs exactly the pipeline it ran before.

A `<select>`, not a numeric input, so it is deliberately kept out of the
numeric-coercion list in settings.php that would otherwise mangle the string.
An unrecognised value maps to auto rather than being written through — the
daemon clamps unknown values itself, but a config file we write should not carry
a backend that does not exist, and a typo must never take channels off air.

Touches the six places every fanout_* setting lives: migration 017 for upgrades,
database.sql for fresh installs (column, INSERT list and VALUES — 276/276 still
aligned), the FanoutConfig mapping, the settings UI, all seven language files,
and the unit test. SettingsService needs no change: its whitelist is only for
checkboxes, and verifyPostTable already maps a posted field to its column.

Verified against the real daemon end to end: the panel writing "ffmpeg" boots
xc_fanout with backend=ffmpeg, and changing it to "auto" is picked up live on
the next config poll. Read-modify-write still preserves daemon keys the panel
does not manage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGCQcWGMg1Wn8RdCxYoioN
2026-09-09 19:39:17 +00: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 33f6124ed0 fix(hls): keep MEDIA-SEQUENCE monotonic across the off-air<->live transition
The off-air placeholder (OffAirHandler) numbers its loop floor(time()/10)
(~1.7e8) while the xc_fanout daemon restarts its own HLS counter from 0 per
stream. On an on-demand cold start a player saw the off-air sequence (~1.7e8)
then the live one (~1) — a ~10^8 BACKWARD jump, which HLS forbids, stalling
playback during warm-up.

Re-anchor the live sequence (in HLSGenerator::tokenizeDaemonPlaylist) to the
same wall-clock base via a persisted per-stream offset so it only ever
advances: it stays >= the off-air line and >= the last live window, while
still incrementing by the daemon's per-segment delta. The off-air handler is
left untouched (it is stream-agnostic). Adds HlsSequence with a pure,
unit-tested reconcile() step plus HlsSequenceTest.
2026-09-06 00:40:32 +03:00
Divarion_D 10716fd9ec feat(fanout): expose xc_fanout tuning knobs in settings (DB + UI)
The panel only owned two derived fanout config keys (hls_target_sec ←
seg_time, prebuffer_max_sec derived); the daemon's other tuning knobs lived
only in its config.json and could not be set from the panel. Promote them to
real settings so admins control the whole config — the unconditional
fanout_sync (previous commit) then keeps config.json in step.

Adds 9 settings columns (fanout_hls_window, fanout_grace_sec,
fanout_write_timeout_sec, fanout_chunk_bytes, fanout_max_gop_bytes,
fanout_source_insecure, fanout_default_prebuffer_sec,
fanout_idle_buffer_grace_sec, fanout_idle_buffer_ratio) with defaults and
ranges mirroring the daemon schema (XC_VM_Fanout internal/config/config.go):
migration 014 + install SQL, inputs on the admin Settings page (numeric plus
one switchery checkbox), and the source_insecure toggle in the settings
boolean-normalization list.

FanoutConfig::desired() now writes all 11 keys, clamped to the daemon's own
ranges so a bad panel value can't push it into a pathological state;
hls_window comes from the setting (not the daemon snapshot) and feeds the
prebuffer_max_sec derivation. prebuffer_max_sec / hls_target_sec stay derived.

Adds FanoutConfigTest (mapping, clamps, derived ring, idempotency,
unknown-key preservation). Full suite green.
2026-09-01 16:03:39 +03:00
Divarion_D 567641137d test: fix stale update-channel tests and define BIN_PATH in bootstrap
Three unit tests were failing against correct production code:

- FfmpegPathsTest errored on an undefined BIN_PATH constant. The test
  bootstrap defined FFMPEG_BIN_* but not BIN_PATH, so FfmpegPaths::binary()
  faulted before it could fall back. Define BIN_PATH to a fixture-less path
  so version lookups miss and the FFMPEG_BIN_40 fallback is exercised.
- GitHubReleasesTest expected the old 'unstable' channel name; the channel
  was renamed to 'beta' ('unstable' kept as a legacy alias). Update the
  cache-switch test to expect 'beta' (and assert the alias normalizes), and
  make the reject-invalid test use a genuinely unknown channel ('nightly').

Full suite now green (461 passing).
2026-08-31 22:22:34 +03:00
Divarion_D c27950093f refactor(admin): replace admin_constants globals with reference classes and enums
Move the 17 global lookup arrays from resources/data/admin_constants.php
into PSR-4 classes and enums, eliminating the last big pile of `global`
reference data.

New XcVm\Core\Reference\* (const + static accessors, FfmpegBinaries-style):
GeoReference, LocaleReference, DeviceReference, UiReference,
PermissionReference, StatusBadge. New XcVm\Core\Enum\* (backed enums with
label()/options(), ModuleState-style): Theme (int), ResellerAction and
ClientFilter (string).

All consumers migrated off `global $rX` / `$GLOBALS['rX']` and the
BaseAdminController extract bridge: TableController, DashboardController,
SearchAjaxController, SettingsController, ResellerAPI and
ResellerLoginController call the classes directly, and 21 view templates
import them via `use`. The extract whitelist entries, the bootstrap
require and admin_constants.php itself are removed.

Fixes a latent bug: TableController read $rClientFilters without a
`global`, so the client-request log showed raw status codes instead of
labels; ClientFilter::labelFor() now resolves them.

The reference classes and the three new enums are excluded from the
load-balancer build (all consumers are already MAIN-only).

Adds unit tests for the enums, StatusBadge and the reference data.
2026-08-31 20:58:23 +03:00
Divarion_D 19c8865501 chore(dead-code): remove SegmentReader orphaned by E2
E2 deleted live.php's non-proxy chase-read — the only user of SegmentReader
(playlist segment extraction). Remove the now-dead class, its unit test, and the
doc references. SignalSender (still used by segment.php) and CacheReader (used
widely) are kept.
2026-08-20 18:39:09 +03:00
Divarion_D 8b4342cc75 feat(streaming): P2/C — off-air detection for the daemon proxy path
Phase C parity (ADR 0003): restore the legacy startProxy off-air behaviour
for daemon-served proxy streams. After registering a proxy source, live.php
calls FanoutClient::probe($id, on_demand_wait_time*1000) → daemon
POST /probe/<id>, which prewarms the puller and waits for the source to
produce data. If it stays dead, live.php shows the not-on-air page instead
of X-Accel'ing the viewer onto an empty/hanging daemon stream. A warm stream
(already producing) returns immediately, so only a cold/dead source pays the
wait — same as the legacy path.

Non-proxy off-air was already covered (the process block waits for the
playlist, and A3 gates on isStreamFed/has_data).

Box-validated: dead-source proxy → not-on-air video after on_demand_wait_time
(20s here); live-source proxy → fast play via the daemon (4.4 MB valid TS).
PHPStan clean, 11 FanoutClient tests.

Refs ADR 0003 (Phase C). Daemon: XC_VM_Binaries 0.4.0.
2026-08-16 19:08:16 +03:00
Divarion_D 401e2307e4 feat(streaming): P2/A3 — route non-proxy live viewers through the daemon
Completes Phase A (ADR 0003): non-proxy live streams now both feed the
daemon (A2 tee) and are SERVED by it. At live.php's non-proxy delivery
point, when the daemon is reachable and FanoutClient::isStreamFed($id)
(control GET /streams/<id> → has_data), hand the byte path to nginx→daemon
via X-Accel-Redirect: /xc_fanout/<id> — like proxy mode — instead of
pinning the worker in the per-viewer .ts chase-read. Daemon down / not fed
⇒ the legacy chase-read runs (the reachability rollback).

The tee feeds the daemon from the same ffmpeg that writes the on-disk HLS,
so by delivery time (playlist ready) the daemon has data. Ingest-fed
streams report running:false (no puller), so the gate is has_data.

Box-validated: 6 concurrent non-proxy viewers on stream 569 all served by
the daemon (daemon_conns=6) with php-fpm workers flat (not 1-per-viewer),
valid TS. PHPStan clean, 10 FanoutClient tests.

ADR 0003: A3 done; added a ⚠️ TODO that LLOD v3 (LlodCommand, streams.llod=2)
has no daemon path (separate PHP segmenter) and must be integrated before
Phase D — llod=0/1 already covered via buildLive+tee.

Refs ADR 0003 (Phase A).
2026-08-16 18:37:42 +03:00
Divarion_D 070d89b3c4 feat(streaming): P2/A2 — tee non-proxy live ffmpeg into the xc_fanout daemon
Bring non-proxy live streams into the daemon (ADR 0003, Phase A). Before
launching a standard live stream's ffmpeg, StreamProcess registers a daemon
ingest (FanoutClient::registerIngest → PUT /ingest/<id>, returns the socket)
and, when the daemon accepted it, builds the HLS output through the `tee`
muxer so one ffmpeg feeds BOTH the existing on-disk HLS and the daemon:

  -map … -f tee "[f=hls:…]<id>_.m3u8|[f=mpegts:onfail=ignore]unix:<ingest.sock>"

- onfail=ignore keeps the on-disk HLS alive if the daemon output drops
  (verified: ffmpeg "continuing with 1/2 slaves"), so a daemon crash never
  takes the stream down. A plain second output would abort ffmpeg entirely.
- Daemon unreachable ⇒ registerIngest returns null ⇒ the original on-disk-only
  HLS output runs, unchanged (the reachability rollback). Loopback/delay
  keep the legacy output.
- Two tee-muxer gotchas handled: tee needs an EXPLICIT -map (plain outputs use
  automatic selection; tee errors "does not contain any stream") — reuse the
  stream's map or -map 0 -copy_unknown; and `individual_header_trailer` is a
  segment-muxer option the hls muxer ignores in flag form but rejects fatally
  inside a tee slave, so it is omitted.
- stopStream unregisters the daemon ingest; FanoutClient::call() now checks the
  socket exists so unregister is a no-op when the daemon isn't deployed.

Box-validated (real panel + ffmpeg, stream 569): one ffmpeg feeds on-disk HLS
(5 segs, m3u8) AND the daemon (/live TS + /hls in-RAM), no errors; panel
playback via the legacy path unaffected (1.69 MB valid TS). PHPStan clean,
441 unit tests pass.

Refs ADR 0003 (Phase A).
2026-08-16 18:16:45 +03:00
Divarion_D 6d3c84a473 feat(streaming): P2/S3 — proxy-mode live TS via xc_fanout, flag-gated
Route proxy-mode live TS through the xc_fanout daemon behind the
`live_fanout` settings flag, taking PHP out of the byte path. When the
flag is on, live.php runs its existing auth unchanged, then instead of the
socket_read→echo relay it registers the source with the daemon over the
control unix socket and hands the viewer to nginx via
`X-Accel-Redirect: /xc_fanout/live/<id>` — the FPM worker is freed
immediately, same pattern as P1 HLS delivery. Flag off (the default) keeps
the legacy relay; a failed registration falls through to it in-request, so
a down daemon degrades gracefully.

- FanoutClient: control-socket client (PUT/DELETE /streams/<id> over
  FANOUT_CTL_SOCK via cURL unix socket) + buildSource(), a pure mirror of
  ProxyCommand's stream_source/user_agent/proxy/cookie extraction whose
  JSON matches the daemon's streamConfig.
- Paths: FANOUT_RUN_PATH / FANOUT_CTL_SOCK / FANOUT_HTTP_SOCK under /run.
- nginx: internal `location ^~ /xc_fanout/live/` → proxy_pass unix daemon
  client socket (buffering off, long read timeout).
- FanoutClientTest: 8 cases covering source-config parity and the
  empty-urls short-circuit.

Refs ADR 0002. Daemon lives in XC_VM_Binaries (not this repo).
2026-08-16 15:54:40 +03:00
Divarion_D fda7f41258 refactor(ministra): move Stalker portal from module into core (src/Ministra)
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.
2026-08-11 21:41:11 +03:00
Divarion_D 9c7231c6ca refactor(core): remove unused BoundaryInterface marker
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).
2026-08-11 19:22:09 +03:00
Divarion_D 2f8b36d1f3 refactor(signal): unify deferred-work signals behind SignalQueue
Signals — the file-backed queue of deferred DB writes drained by the
cache-handler daemon — were produced two different ways: five call sites went
through RedisManager::setSignal() while getStreamingUserInfo inlined the same
file_put_contents(). Worse, the writer lived on RedisManager yet wrote a *file*,
not Redis — a misleading home that made the two paths look like different
transports (they were byte-identical).

Introduce XcVm\Infrastructure\Signal\SignalQueue as the single owner of the
on-disk contract (cache_<md5(key)> holding [key, data]):
- push()    - the one producer API; all 7 call sites use it now
  (UserRepository x4, auth.php x2, BruteforceGuard x1)
- pending() - the drain API; CacheHandlerCommand reads through it instead of its
  own glob + json_decode
- pathFor() / PREFIX - path helpers

RedisManager::setSignal() stays as a thin @deprecated alias delegating to
SignalQueue::push() for any out-of-tree callers.

Behaviour unchanged (same files, same format, same consumer switch). Adds
SignalQueueTest (format / idempotency / drain / malformed). Validated live:
player_api/testxc still authenticates (auth=1) with SignalQueue autoloaded.

Verified: php -l, phpunit (435 tests, 973 assertions), make gates.
2026-08-09 13:01:57 +03:00
Divarion_D 8f72218ea6 refactor(user): consolidate getStreamingUserInfo/getUserInfo shared blocks
The two methods were ~90% duplicate, differing only in how they obtain
settings/cache/bouquets and in the divergent GeoIP + signal backends
(GeoIPService/file-signals vs GeoIP/Redis) that stay inline. Extract the
identical blocks into private helpers, called by both:

  loadUserRow             - credential resolution (token/cache file or DB),
                            resolving $rUserID by reference so the cached
                            re-verification keeps its exact behaviour
  verifyCachedCredentials - cached access-token / username+password re-check
  decodeUserFields        - JSON line fields -> arrays
  resolveOutputFormats    - allowed output-format keys
  aggregateBouquetIds     - bouquet -> channel/series/vod/live/radio id lists
  resolveCategoryIds      - bouquet -> category ids

Also de-obfuscate the remaining `if (cond) {} else { body }` blocks in both
methods. Net -74 lines despite adding the shared helpers (~240 lines of
duplication removed). Behaviour preserved; the file-vs-Redis signal divergence
is intentionally left as-is (separate decision).

Tests: UserRepositoryTest covers the pure helpers (aggregate/category/decode/
verify), 12 tests total. Validated live: player_api/testxc returns auth=1,
Active, allowed_output_formats=[m3u8,ts,rtmp]; on-demand /live start still works.

Verified: php -l, phpunit (430 tests, 962 assertions), make gates.
2026-08-09 01:19:04 +03:00
Divarion_D d9138f228a refactor(user): simplify getE2Info/getUserInfo, fix the isp_asn miss bug
- getE2Info: collapse the obfuscated `if (cond) {} else { body }` chain into
  straight positive-form ifs; drop the redundant re-init of pair_line_info.
- getUserInfo: same de-obfuscation of the ISP block, and fix the same
  "Undefined array key isp_asn" bug already fixed in getStreamingUserInfo — the
  ISP-persist step ran even on a GeoIP miss (con_isp_name null), reading the
  undefined isp_asn key and writing a null isp_desc/as_number to the line.
- Extract the persist predicate both methods shared into a pure, tested
  UserRepository::ispChanged() helper (+ tests/Unit/UserRepositoryTest.php).

Verified: php -l, phpunit (423 tests, 944 assertions), make gates.
2026-08-09 01:03:16 +03:00
Divarion_D 4902e953a0 fix(streaming): fill the on-demand prebuffer before the first TS burst
For continuous-TS delivery (/play/<token>/ts) the client's whole playback buffer
is the initial prebuffer burst — the feed loop then runs in real time, so the
buffer never grows past it. On a cold on-demand start the server began serving
the instant the playlist first appeared, when only the 1-2 fast-start (2s)
segments existed, so selectSegments could only return ~2s and the client was
stranded at ~2s of buffer regardless of client_prebuffer.

Wait until the playlist actually holds client_prebuffer seconds (bounded by
on_demand_wait_time, bailing if the stream stops) before the first read. Clients
only — restreamer chains keep their low-latency behaviour; warm streams already
satisfy the condition and don't wait. Adds SegmentReader::playlistBufferedSeconds()
(sum of #EXTINF) as the gate.

Validated live on stream 567: at cold start OLD served 1 seg / 2.0s; NEW served
5 seg / 10.0s after a 0.2s wait (fast-start pre-generates segments, so the added
startup latency is negligible).

Verified: php -l, phpunit (SegmentReaderTest 10/10), make gates.
2026-08-09 00:04:04 +03:00
Divarion_D 9168853419 refactor(monitor): dedupe segment-duration handling into persistSegmentDuration
The label195 and label562 probe blocks each clamped the probed of_duration to
10s, wrote it to the stream's _.dur file and raised $rSegmentTime -- identical
seven-line cores. Extract them into a shared persistSegmentDuration() helper
returning [clamped probe, updated segment time], with unit tests (temp
STREAMS_PATH). No control-flow change; behaviour preserved.

Verified: phpstan level 5 green, phpunit 11 monitor tests green, make gates green.
2026-08-08 20:03:30 +03:00
Divarion_D 0020b11bec refactor(monitor): extract resolveStreamCodecMeta from the label562 block
The post-start block derived player compatibility, audio/video codec names and
the resolution (snapped to the nearest standard height) inline from the
stream_info JSON. Pull it into a pure resolveStreamCodecMeta() helper with unit
tests. No control-flow change; behaviour preserved.

Verified: phpstan level 5 green, phpunit 9 monitor tests green, make gates green.
2026-08-08 19:58:07 +03:00
Divarion_D 5e92e521da refactor(monitor): extract isAutoRestartDue from the label195 goto chain
The scheduled auto-restart check was three chained `if (!cond) goto label195;`
statements testing the configured days + HH:MM against the current
weekday/hour/minute. Replace them with a pure isAutoRestartDue() predicate
(injectable for tests) and a single `goto label195`. Two more gotos gone
(47 -> 45), and the schedule logic is unit tested.

Verified: phpstan level 5 green, phpunit 6 monitor tests green, make gates green.
2026-08-08 19:54:59 +03:00
Divarion_D e7ceb42699 refactor(monitor): extract parseFrameRate from the FPS-check goto cluster
First step of untangling MonitorCommand::execute()'s obfuscated goto control
flow. The FPS baseline computation flattened a "30/1" -> 30.0 rational parse
across five labels (label768/780/1047/1052/1057). Pull the pure parse into
parseFrameRate() with unit tests; the label768 entry and the label1847 exit are
kept so control flow is unchanged -- four labels and their gotos are gone.

Guards a zero denominator (PHP 8 would otherwise throw on "x/0") -- an input
ffprobe does not produce, so behaviour is unchanged in practice.

PHPStan cannot see the self::parseFrameRate call through the surrounding goto
maze and reports the method unused; baselined until the label768 region is
destructured, then the entry drops out.

Verified: phpstan level 5 green, phpunit 408/408, make gates green.
2026-08-08 19:28:37 +03:00
Divarion_D 1c9d837761 fix(streaming): make HLS prebuffer duration-based, not segment-count
An on-demand stream emits short (2s) fast-start segments before it ramps up to
seg_time. getPlaylistSegments picked intval(client_prebuffer / seg_time)
segments -- with the default 10s prebuffer / 10s nominal seg_time that is 1
segment, i.e. only ~2s of actual buffer at cold start, which players surfaced as
"caches 2 seconds". Persistent streams (segments already ~10s) were unaffected:
1 segment = 10s.

Select the newest segments whose #EXTINF durations sum to at least the requested
prebuffer seconds instead, so client_prebuffer means real seconds regardless of
segment length. Fast-start (hls_init_time 2) is kept. Falls back to the old
nominal count when the playlist has no #EXTINF lines; the -1 (all) and 0 (index)
modes are unchanged. Split into a pure selectSegments()/parseSegmentDurations()
so the selection is unit tested.

Verified: phpstan level 5 green, phpunit 405/405, make gates green.
2026-08-07 22:36:44 +03:00
Divarion_D 444483d7b4 feat(streaming): add pure buildLive() assembler with shadow-diff (no flip)
Phase 1 of extracting the live ffmpeg command assembly out of startStream.

buildLive(array $data): string is a byte-faithful copy of the inline assembly,
fed from a prepared $data array instead of loop-local state, with the delay
playlist I/O and segment-start/sleep left in startStream (arriving via
$data['segmentStart']/['delayActive']). The one non-verbatim change is the
ac3/eac3 dts_legacy bin switch, now a local reassignment instead of mutating the
$rFFMPEG_CPU/$rFFPROBE globals (the $rFFPROBE write was already dead). Homed on
StreamProcess for now so the private output/logo/aac helpers resolve via self::;
a FFmpegCommand facade is a follow-up.

startStream computes buildLive() alongside the inline assembly and error_log()s
any divergence, but still executes the inline $rFFMPEG -- shadow mode, no flip.
Once the diff log stays empty on real traffic the call site can switch over.

Characterisation tests cover simple/custom_ffmpeg/loopback/delay/rtmp branches
and assert no unresolved {TOKEN} survives.

Verified: phpstan level 5 green, phpunit 397/397, make gates green.
2026-08-07 21:50:35 +03:00
Divarion_D 2364aae6ee refactor(streaming): extract delay resume helpers from startStream
The delayed-HLS branch inline-computed two things: the resume segment number
parsed from the existing delay playlist (last-or-previous line's _<n>.ts index),
and the delay sleep seconds (delay_minutes*60 reduced ~10s per already-produced
segment, floored at 0). Pull both into pure resolveDelaySegmentStart() and
resolveDelaySleepTime() helpers with unit tests. The playlist merge and the
short-playlist abort stay inline. No behaviour change.

Verified: phpstan level 5 green, phpunit 391/391, make gates green.
2026-08-07 21:01:59 +03:00
Divarion_D 39090e37ba refactor(streaming): extract hasSkipFFProbe and skipFFProbeOutput from startStream
The ffprobe-skip branch of the source loop hand-rolled two things: a scan of the
stream arguments for skip_ffprobe == 1, and the literal assumed h264/aac mpegts
result used when the scan matched. Give both names via pure helpers and cover
them with unit tests. No behaviour change.

Verified: phpstan level 5 green, phpunit 385/385, make gates green.
2026-08-07 20:54:14 +03:00
Divarion_D 2d885af262 refactor(streaming): extract aacBitstreamFilter from startStream
The {AAC_FILTER} substitution carried the rule inline: emit -bsf:a
aac_adtstoasc only when a copied AAC audio stream is muxed into a non-FLV
container. Give that rule a name and a test via a pure aacBitstreamFilter()
helper. No behaviour change.

Add unit tests: applied for copied AAC in mpegts/empty container, skipped for
FLV, skipped when the codec is not AAC or the output is not a copy.

Verified: phpstan level 5 green, phpunit 382/382, make gates green.
2026-08-07 20:46:10 +03:00
Divarion_D f99048b79f refactor(streaming): extract resolveStreamCodecMeta from startStream
The codec-metadata derivation persisted for a started live stream (player
compatibility, audio/video codec names, resolution snapped to the nearest
standard height) was an inline block reading ffprobe output. Pull it into a
pure resolveStreamCodecMeta() helper taking the ffprobe array and the
player_allow_hevc flag and returning [compatible, audio, video, resolution].

The only external input, SettingsManager::getAll()['player_allow_hevc'], is now
passed in (getAll() just returns the cached settings array, so hoisting the read
out of the guard is free). Add unit tests: non-array/missing-codecs defaults,
compatible h264/aac, resolution snapping, HEVC gating by the allow flag, and
incompatible-but-reported codecs.

Verified: phpstan level 5 green, phpunit 379/379, make gates green.
2026-08-07 20:43:37 +03:00
Divarion_D 8973f5580f refactor(streaming): extract appendHeaderArgument from startStream
The X-XC_VM-Detect / X-XC_VM-Prebuffer header injection was duplicated three
times in startStream: each block looped the argument list to append the header
to an existing 'headers' entry and added a new one if absent. Collapse the three
copies into a single appendHeaderArgument() helper and drop the shared
$rProcessed flag they threaded through (it leaked state between the blocks).

Behaviour is unchanged: the helper uses a local applied flag, and the three call
sites produce the same argument lists as before. Add unit tests covering
append-to-existing, create-when-absent, append-to-every-headers-entry, and the
empty-list case.

Verified: phpstan level 5 green, phpunit 373/373, make gates green.
2026-08-07 20:32:51 +03:00
Divarion_D b88ae60dc1 refactor(streaming): extract startStream probe/failover helpers and default its command variables
Continue breaking down StreamProcess::startStream:
  - extract resolveProbeSettings() (ffprobe/analysis timing) and
    rotateSourcesPastCurrent() (source-failover ordering) as private static
    helpers, with unit tests locking in their behaviour;
  - simplify 15 inverted empty-if/else blocks across the class into a single
    positive condition (no behaviour change).

Also fix the latent undefined-variable bugs the refactor surfaced under PHPStan:
startStream assembles the ffmpeg command from state produced inside the source
failover loop (which closes before the command is built) and inside the
non-custom_ffmpeg branch. On an empty source list, or when a custom ffmpeg line
is used, several variables were read while possibly undefined -- masked at
runtime only by ternary guards and suppressed in the PHPStan baseline. Default
them at the two scopes that own them:
  - loop-scoped, read after the loop: $rSource, $rRealSource, $rStreamSource,
    $rProtocol, $rFFProbeOutput
  - non-custom_ffmpeg-branch-scoped, read in the {MAP}/{GEN_PTS}/{READ_NATIVE}
    substitution and delay sleep: $rMap, $rGenPTS, $rReadNative, $rSleepTime
and drop the 9 now-obsolete entries from phpstan-baseline.neon.

Behaviour is unchanged on the happy path (the loop/branch overwrite the
defaults) and the defaults match the existing runtime ternary-guard results.

Verified: phpstan level 5 green (whole project, entries un-suppressed),
phpunit 369/369, make gates green.
2026-08-07 20:20:09 +03:00
Divarion_D 0f773b9758 refactor(streaming): extract map/scodec/source/HLS/FLV helpers with tests
Continue breaking up createChannelItem, startMovie and startStream by pulling
five self-contained blocks into pure, tested helpers:

- resolveOutputMap()          — VOD -map selection (custom > strip-subs > copy-all)
- subtitleCodecForContainer() — mp4->mov_text, mkv->srt, else copy
- resolveChannelSource()      — channel source string -> [serverId, path], rewriting
                                s:<server>:<path> to the remote getFile API URL
- buildHlsMpegtsOutput()      — HLS/mpegts segmenter output args
- buildFlvOutput()            — shared FLV output wrapper (RTMP relay + external
                                push, previously two identical lines)

The startStream extractions pass the conditional $rOptions/$rFLVOptions in as
parameters, so the control flow is unchanged. Behaviour preserved.

StreamProcessMovieOutputTest covers map precedence, container->codec mapping,
every source-resolution branch, HLS output assembly, and FLV wrapping.

Verified: php -l clean, make gates green, phpunit 360/360 green.
2026-08-07 19:31:18 +03:00
Divarion_D 0bb87452ad fix(streaming): import and map all VOD subtitles, not just the first
buildSubtitleImport nested the metadata loop inside the import loop and reused
the outer counter $i. The inner loop advanced $i to count(files), so the outer
import loop exited after its first iteration: only the first subtitle was
imported (-sub_charenc -i), while -map/-metadata was still emitted for every
file — mapping ffmpeg inputs that were never added. Multi-subtitle movies got a
single imported track plus dangling -map references (broken output).

Split the nested loop into two siblings — import every file, then map every
file — and cache the count. All configured subtitles are now imported and
mapped to the correct input index.

Add tests/Unit/StreamProcessSubtitleTest.php covering empty input, a single
local subtitle, remote-server fetch, and the multi-subtitle case that
reproduces the defect (import count was 1, must equal N). Verified red→green
against the pre-fix code.

Verified: php -l clean, make gates green, phpunit 336/336 green.
2026-08-07 19:04:16 +03:00
Divarion-D f107e5f769 feat(navbar): make admin profile dropdown registry-driven
The top-right profile dropdown was a hardcoded HTML block in
  admin/header.php, so modules could not add or remove entries and the
  plex/watch "settings" links were baked into core.

  Move the menu into the NavbarRegistry: CoreNavbarProvider now registers
  the core items (edit profile, settings, backups, cache, modules, logout)
  under a reserved 'profile' parent, and header.php renders
  NavbarRegistry::getChildren('profile') adaptively. There is intentionally
  no top-level 'profile' node, so the items never leak into the main
  navigation. Order 100–980 is reserved for module-provided links.

  Extract NavbarRegistry::collapseDividers() to drop separators orphaned by
  permission filtering (leading, trailing, consecutive), and reuse the
  shared _xc_nav_visible()/_xc_nav_label() helpers by defining them once
  near the top of the header. Remove the hardcoded settings_plex /
  settings_watch entries — those now belong to their owning modules.

  Add NavbarProfileMenuTest covering collapseDividers, the core 'profile'
  contract (no top-level leak, edit first / logout last, permission gates),
  and the module-integration contract (reserved order slots, divider
  collapse when folder_watch_settings is absent).
2026-07-29 19:37:43 +03:00
Divarion-D 2036f9db20 refactor: optimize uptime formatting and update data provider methods to static 2026-07-10 17:40:37 +03:00
Divarion-D 0532b82c96 refactor(tests): remove deprecated global $db exemptions from ArchitectureTest 2026-07-07 21:51:51 +03:00
Divarion-D f8a37947b1 feat(tmdb)!: fold the tmdb module into core, replace stale standard-set copies
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.
2026-07-05 20:17:31 +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 cb4dfe04b5 feat(modules): resolve module update sources + weekly update-check cron
ModuleUpdateChecker resolves the latest available version per module by source:
git (via GitHubReleases), url (version.json), platform (best-effort), bundled
(no-op). ModuleUpdatesCronJob (cron:module_updates) records available_version
into config/modules.php; migration 008 registers it to run weekly. Network-free
unit tests cover the routing.
2026-07-03 20:10:41 +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 5863d1bd5e chore: remove legacy XC_Autoloader and update autoloading documentation 2026-06-26 16:35:18 +03:00
Divarion-D 2bed01e660 chore(psr4): final phase step 2 — remove the legacy autoload.php entirely
The XC_Autoloader fallback was already retired (no-op stub); this deletes it for
good. Resolution is now 100% Composer PSR-4 (+ ModuleLoader for modules), no
legacy scanner, no class-map cache.

- Move `define('MAIN_HOME', ...)` into bootstrap.php (autoload.php used to define
  it); bootstrap.php now requires only vendor/autoload.php.
- Drop `\XC_Autoloader::clearCache()/warmCache()` from StartupCommand.
- tests/bootstrap.php: locate-guard + require switched to vendor/autoload.php.
- Entry points that required autoload.php directly — Public/index.php,
  Public/admin/index.php, Public/stream/index.php, Public/progress/index.php,
  ministra/portal.php and Admin/Reseller TableController — switched to
  vendor/autoload.php (defining MAIN_HOME where they did not already). These were
  not in the plan's checklist; found via grep during execution.
- phpstan.dist.neon: drop src/autoload.php from scanFiles.
- Makefile: drop autoload.php from LB_ROOT_FILES.
- deleted_files.txt: add autoload.php (client cleanup on update).
- AutoloadOrderTest: now asserts the XC_Autoloader class and file are gone.
- git rm src/autoload.php.
- PSR4_MIGRATION_PLAN.md: mark final-phase step 2 done.

Verified: grep XC_Autoloader:: = 0; php -l clean; PHPStan no errors; PHPUnit
303/303; make gates pass; bootstrap smoke — MAIN_HOME + XC_Bootstrap present,
XC_Autoloader gone, only the Composer autoloader registered.
2026-06-25 21:11:00 +03:00
Divarion-D fd35f00c4d fix(views): import migrated classes at top of 5 admin view templates
enigma, episode, episodes, process_monitor and stream_view referenced migrated
classes (UserRepository, BouquetService, StreamRepository, RequestManager,
SettingsManager, ...) by short name with either no `use` or a `use` placed
*below* the first usage. PHP imports outside the top scope are positional, so the
short name resolved to a now-nonexistent global class — a runtime fatal on those
admin pages (php -l passes; not covered by PHPStan/PHPUnit). The migration's
automated `use` insertion missed them due to the interleaved HTML / short-tag
(<? , <?=) structure, and the later php-cs-fixer pass stripped some as 'unused'
because it could not see usage inside short-tag blocks.

- Consolidate every needed `use XcVm\...;` into a single top-of-file PHP block.
- Exclude Public/Views and Modules/*/views from php-cs-fixer (no_unused_imports
  is unreliable on short-tag templates); their import correctness is enforced by
  the new check_procedural_use gate instead.

Verified: php -l (short_open_tag=1) clean; PHPStan no errors; PHPUnit green.
2026-06-25 20:57:37 +03:00
Divarion-D 43e2f275b1 chore(psr4): phase 3 — namespace module sub-classes
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.
2026-06-25 19:12:48 +03:00
Divarion-D 3377236d77 chore(psr4): phase 3 (Cli) — namespace Cli + switch console.php to FQCN discovery
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.
2026-06-25 19:03:39 +03:00
Divarion-D 62d2d1d942 chore(psr4): phase 3 — namespace Streaming layer
Namespace all 17 Streaming classes into XcVm\Streaming\<Subdir> (Auth, Balancer,
Codec, Delivery, Health, Lifecycle, Protection + root AsyncFileOperations,
StreamingBootstrap, TS).

- Add namespace to every class; add use to referrers across src/ and tests/;
  convert leading-backslash refs from earlier commits (\FfmpegPaths,
  \FFprobeRunner, \ProcessChecker, \StreamingBootstrap) to FQCNs.
- Qualify built-ins/ioncube inside Streaming (\DateTime, \DateTimeZone,
  \Exception, \XC_VM).
- FfmpegPathsTest: new ReflectionClass('FfmpegPaths') string → ::class FQCN.
- phpstan-baseline.neon regenerated (291).

Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; no mangled FQCNs;
leading-backslash re-sweep clean.
2026-06-25 18:55:35 +03:00
Divarion-D 42d9ca8ea0 chore(psr4): phase 3 — namespace Domain layer
Namespace all of Domain into XcVm\Domain\<Subdir> (33 classes across Bouquet,
Device, Epg, Line, Security, Server, Stream, User, Vod).

- Add namespace to every Domain class; add use to referrers across src/ and
  tests/; convert the leading-backslash refs qualified in earlier Core commits
  (\UserRepository, \ServerRepository, \BouquetService, \CategoryService,
  \ConnectionTracker, \BlocklistService, ...) to their FQCNs.
- Qualify still-global / built-in deps inside Domain with leading backslash
  (\RedisManager, \TMDB, \WatchService, \FFprobeRunner, \ProcessChecker,
  \Exception, \DateTime, \PDO, ...) — Infrastructure/Streaming/module classes
  migrate later.
- BruteforceGuard: string guards class_exists('ServerRepository'/'BlocklistService')
  → ::class FQCN; drop the now-always-true method_exists check.
- phpstan-baseline.neon regenerated (292→291).

Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; leading-backslash
re-sweep across Domain classes is clean.
2026-06-25 18:39:28 +03:00
Divarion-D da291e20f8 chore(psr4): phase 3 (Core) — namespace remaining Core subdirs
Namespace the rest of Core, completing the Core layer:
- Backup, Boundary, Cache (CacheInterface/FileCache/RedisCache), Diagnostics,
  GeoIP (GeoIPService/MaxMindUpdater), Init (LegacyInitializer), Localization
  (Translator), Process (Thread/Multithread/ProcessManager), Updates
  (GitHubReleases), Util (NetworkUtils, AdminHelpers, Encryption, GeoIP,
  ImageUtils/ImageResizeService, Mobile_Detect, StreamUtils, SystemInfo,
  TimeUtils), Validation (InputValidator) → XcVm\Core\<Subdir>.
- Rename GithubReleases.php → GitHubReleases.php so the file matches its class
  name (PSR-4 is case-sensitive); fix the WebApiBootstrap require accordingly.
- Qualify built-ins (\Exception, \DateTime, \DateTimeZone, \Redis,
  \RuntimeException, \BadMethodCallException, ...) and still-global deps
  (\ServerRepository, \CacheReader, \DatabaseFactory, \BouquetService,
  \CategoryService, \FfmpegPaths, \StreamSorter, \XC_VM). LegacyInitializer's
  Domain calls stay \-qualified (the known Core→Domain exception).
- Add use to referrers; fix leading-backslash refs from earlier commits; fix
  string class refs class_exists('Translator'/'NetworkUtils'/...) → ::class.
  Remove the duplicate global 'use BoundaryInterface;' in MinistraModule.
- phpstan-baseline.neon regenerated.

Core is now fully namespaced (procedural constant/error/RequestGuard files stay
global by design; M3uParser/PhpM3u8 remain vendored). Verified: php -l clean;
PHPStan no errors; PHPUnit 295/295; leading-backslash re-sweep clean.
2026-06-24 22:35:33 +03:00