Commit Graph
1331 Commits
Author SHA1 Message Date
Divarion_D 0e2d64cdfd feat(ffmpeg): dynamic build discovery + capabilities, wire ffmpeg_gpu, drop 4.0 hardcoding
Discover the bundled FFmpeg builds from bin/ffmpeg_bin/<version>/ instead of a
hardcoded version list, and expose what each build can do.

- FfmpegBinaries (new): scans real version folders (skips *.backup.*), probes each
  binary's hardware encoders (nvenc/qsv/vaapi/…), flags GPU capability, caches the
  result to tmp/ffmpeg_capabilities.json keyed by a folder-mtime fingerprint. UI-only.
- FfmpegPaths::resolve() rewritten: builds paths dynamically from the version string
  (is_file check, no shell — stays on the bootstrap hot path) and takes a separate GPU
  version; the switch and the per-version constants are gone. Unknown CPU version falls
  back to the legacy 4.0 build; an empty/invalid GPU version falls back to the CPU build.
- Wire the previously-dead ffmpeg_gpu column: LegacyInitializer passes it to resolve();
  settings.php lists versions from the scanner and adds an ffmpeg_gpu <select> limited to
  GPU-capable builds ("Same as CPU" = empty). No migration — the column already exists.
- Info tab: a per-build FFmpeg table (version, build banner, GPU badge, hw encoders).
- De-hardcode: drop the unused FFMPEG_BIN_71/80 + FFPROBE_BIN_71/80 constants; keep only
  the 4.0 anchor (DTS override in StreamProcess + FfmpegPaths fallback). Llod/Loopback/
  Record now use FfmpegPaths::cpu()/probe() (config-selected version) with the 4.0
  constant as fallback, matching Scanner/Created/Thumbnail.
2026-08-30 14:05:34 +03:00
Divarion_D 47200c1322 refactor(updates): rename the "unstable" update channel to "beta" everywhere
Make 'beta' the canonical update_channel value across the settings UI,
GitHubReleases (prerelease filter + cache-file suffix + setChannel), the
binary/fanout update commands, and the module channel mapping.

'unstable' is kept as a legacy alias so nothing breaks mid-upgrade:
GitHubReleases::normalizeChannel() maps it to 'beta', and the binary commands
still accept it. Migration 011 rewrites existing settings.update_channel
'unstable' -> 'beta', and the settings dropdown normalizes a stale 'unstable' to
show Beta selected before the migration runs. Docs (en) updated.
2026-08-30 00:43:32 +03:00
Divarion_D aa64fef402 chore(admin): relabel update channel "Unstable" as "Beta"
Display-only rename in the settings dropdown; the stored update_channel value
stays "unstable", so GitHubReleases, the binary/fanout update commands and the
module channel logic are untouched (they already treat "beta" as an alias of
"unstable"). No DB migration needed.
2026-08-30 00:38:32 +03:00
Divarion_D 45df86e032 chore(admin): update bundled phpMiniAdmin to 1.9.251125
Refresh the vendored SQL browser (src/Public/Views/admin/database.php) from
phpMiniAdmin 1.9.240801 to 1.9.251125. XC_VM integration is preserved: the
AdminHelpers use-import, the DB_ACCESS_ENABLED gate + goHome(), the session-hash
and is_admin guards, and the empty $DBDEF defaults (connection credentials are
never hardcoded — supplied via the connect form / conn[] cookie).
2026-08-30 00:32:10 +03:00
Divarion_D f9b9e4d794 feat(fanout): sync the daemon config.json from panel settings on save
FanoutConfig does a read-modify-write of the xc_fanout daemon's config.json: read
the current snapshot, overlay only the panel-owned keys (hls_target_sec from
seg_time; prebuffer_max_sec sized to cover the largest per-viewer prebuffer and
the HLS window, floored at 40 and capped at 120), and write it back atomically.
Keys the daemon owns are preserved untouched, so a key it later adds is never
clobbered. No-op when the daemon tree is absent on the node.

Wired into SettingsService::edit() after a successful settings UPDATE; never
throws, so a config write can't break a settings save.
2026-08-30 00:15:44 +03:00
Divarion_D 1986877546 fix(admin-api): return a JSON envelope for stream control, not the raw relay body
StreamAjaxController start/stop/restart (stream + vod) echoed the raw return of
ApiClient::request(). That call relays to the local /admin/api handler, whose
body can come back empty under PHP-FPM (a self-request), blanking the page and
breaking the frontend's `data.result` check — even though the node side-effect
(start/stop) still fired. The refactor comment claimed this mirrored api.php, but
the legacy handler always echoed a fixed {"result":true} after getMultiCURL.

Fire-and-forget the relay and return {"result":true} via $this->ok(), matching
the legacy behaviour and ServerAjaxController::server(). Harden rtmp_kill the same
way: fall back to {"result":false} on an empty systemRequest body (offline node)
instead of echoing nothing.
2026-08-30 00:15:35 +03:00
Divarion_D b3c15d3195 fix(fanout): clear the releases cache on a forced binary update
`fanout_binary force` reinstalls the daemon but resolved the target version from
GitHubReleases' 30-minute on-disk cache, so a forced update could pin a stale
"latest" (e.g. an old v0.7.0) instead of rechecking GitHub. force now calls
clearCache() before getReleases(), so a forced reinstall always resolves against
a fresh response.
2026-08-29 18:37:34 +03:00
Divarion_D 92acefe06c fix(fanout): flock single-instance guard in run.sh + reap orphaned daemons
The supervisor could run in multiple copies (a boot/keepalive race, a manual
start), and a supervisor that died without its child left the daemon orphaned
and still holding the sockets — so several xc_fanout daemons fought over the
bind, each burning RAM and CPU.

run.sh now holds an exclusive flock (fd 9) for its whole lifetime: a second
run.sh fails to take it and exits immediately, so there is never more than one
supervisor — and since each supervisor blocks on exactly one daemon, never more
than one daemon. Before spawning, the loop pkills any stray xc_vm xc_fanout
(a daemon orphaned by a previous supervisor) so a fresh bind never races an
orphan. Start/exit lines are appended to the daemon log for diagnosis. The
RootSignalsCronJob comment is updated to reflect the flock guard.
2026-08-29 18:37:33 +03:00
Divarion_D abcb09c120 docs(ci): version the docs site per release with mike
Add a version selector to the docs and publish one snapshot per release instead
of a single rolling site, so readers can pick the docs matching their installed
version (the docs change release to release).

- mkdocs.yml: enable the Material version selector (extra.version.provider: mike,
  alias: true).
- docs/requirements.txt: add mike==2.1.3.
- pages.yml: deploy with mike, triggered by a release TAG (semver) instead of
  every push to main — publishing is tied to the release because docs/ru is only
  regenerated then. Deploys `X.Y.Z` + the `latest` alias to the gh-pages branch
  and sets latest as default. workflow_dispatch takes an explicit version.
- updates_checklist.md: note the tag-triggered versioned publish.

One-time setup (GitHub UI): Settings → Pages → Deploy from a branch → gh-pages.
2026-08-28 22:36:14 +03:00
Divarion_D 8cc02879c1 docs(dev): document the admin AJAX API and structured search contract
Add a Developer Guide page covering the admin `?action=` JSON endpoints:

- The PSR-4 controllers under Admin\Ajax that replaced the retired ~4985-line
  Views/admin/api.php (PR #173) — BaseAjaxController scaffolding
  (ok/fail/gate/gateAny/requireXhr/json), LineStateTrait, per-area controllers,
  route registration and the dispatchApi → AjaxController fallback order.
- The structured search JSON contract (PR #174): envelope, item shape,
  self-describing actions, per-entity data, and the client-side card renderer;
  plus the note that only the render path changed (matching is unchanged; a
  missing live stream means a stale streams FULLTEXT index).

Wire it into the Developer Guide nav (+ ru nav_translations) and cross-link it
from HTTP Request Handling. English source only; docs/ru is regenerated before
release.
2026-08-28 22:23:46 +03:00
Divarion_D ad0080d557 fix(fanout): respawn the xc_fanout keepalive supervisor if it dies
The xc_fanout daemon is kept alive by bin/xc_fanout/run.sh (a Restart=always
loop that respawns the daemon within ~2s of any exit, SIGKILL included), but
run.sh itself was launched only once by `service boot`. If the supervisor died
(OOM, a stray kill) the daemon was left unsupervised and never came back after
its next exit — nothing re-launched the loop (fanout_binary only pkills the
daemon and relies on it; StartupCommand only fixes its mode).

cron:root_signals (root, every minute, every node) now checks the supervisor is
alive and re-launches it as xc_vm when absent — a cheap pgrep, idempotent
(run.sh self-limits via the socket bind), matching `service boot`. Verified
live: killing both run.sh and the daemon self-heals within one minute.
2026-08-28 22:11:59 +03:00
DanilandGitHub 5a674336aa Merge pull request #174 from Vateron-Media/feature/search-json
feat(admin): global search returns structured JSON, client renders cards
2026-08-28 21:55:53 +03:00
Divarion_D 48298859fa feat(admin): serve global search as structured JSON, render cards client-side
Replace the server-side HTML builder in the `search` admin-ajax action with a
structured JSON contract: each result item now carries `{id, url, text, entity,
data}` and the client renders the card. Permission checks, status resolution and
category/server lookups stay server-side; only markup moves to the browser. This
unblocks the upcoming redesign, where the embedded HTML builder was in the way.

- SearchAjaxController::search() now emits structured items (per-entity `data`
  payloads for stream/movie/channel/radio/episode, series, user, line, mag,
  enigma) plus a `no_results` item for empty responses. Data-gathering is
  unchanged (byte-identical to the previous fuzzy full-text gather).
- New client renderer `assets/admin/js/search.js` (`renderSearchItem`), loaded
  before common.js; the Select2 quick-search `templateResult` now calls it
  (with a loading-state guard) instead of consuming a server `html` field.
- ADR docs/adr/search-json-contract.md documents the envelope, item shape,
  self-describing actions and per-entity payloads.

Note: this changes only the search render path, not search matching. Live
streams missing from results are a stale streams FULLTEXT index on the DB
(rebuild with `ALTER TABLE streams ENGINE=InnoDB`), unrelated to this change.
2026-08-28 21:50:05 +03:00
DanilandGitHub d6c2524a0e Merge pull request #173 from Vateron-Media/refactor/api-ajax-controllers
refactor(admin-api): extract admin/api.php into PSR-4 Ajax controllers
2026-08-28 20:49:52 +03:00
Divarion_D 6f575bed93 docs(admin-api): clean up Ajax controller docblocks
Rewrite the header docblocks of the Admin\Ajax controllers to describe purpose
and actions only, and drop the migration-history commentary (extracted-from-
api.php / ported-verbatim / byte-identical / empty-then-flattened / flagged
notes) from both docblocks and inline comments. Kept the genuinely useful
behavioural notes (no-gate actions, raw-echo responses, {status} envelopes,
in-process report dispatch, shared trait/helpers).

Comments only — no code or behaviour change.

Verified: php -l (all 17), make gates, make phpstan (0), make cs.
2026-08-28 20:30:10 +03:00
Divarion_D d974f668a1 refactor(admin-api): retire the legacy Views/admin/api.php
With all ~90 actions extracted into dedicated Admin\Ajax controllers, the last
tie to the legacy file was AjaxController requiring it as a fallback. Fold that
fallback (XHR guard + {"result":false} for unknown/removed actions) directly
into AjaxController::index() and delete Views/admin/api.php.

The `api` route is unchanged, so an action not peeled off by
Router::dispatchApi() still gets a proper JSON {"result":false} (not a 404).
Admin auth is already enforced by AdminScopeBootstrap before dispatch, so the
controller no longer needs the include/chdir/session dance.

api.php is gone (4985 lines at the start of this refactor -> removed).

Verified: php -l, make gates, make phpstan (0), make cs.
2026-08-28 20:05:47 +03:00
Divarion_D f45eb4cd4a fix(admin): repair enigma bulk operations in the multi action
The bulk multi action resolved its device permission from a map keyed
'enigma2', but the frontend (enigmas.php) sends type=enigma and the switch
matches 'enigma'. So for Enigma2 devices the lookup returned null,
Authorization::check('adv', null) always failed, and every bulk op
(delete/enable/disable/ban/unban/purge/convert) silently returned
{"result":false}.

Resolve the permission directly instead: mag -> edit_mag, enigma -> edit_e2
(the same permission the single-device enigma action uses), via $this->gate().

This is a behaviour change: Enigma2 bulk actions now work.

Verified: php -l, make gates, make phpstan (0), make cs.
2026-08-28 19:49:18 +03:00
Divarion_D 7dc3c84773 refactor(admin-api): extract the multi bulk action; reduce api.php to a stub
Thirteenth and final slice of the admin/api.php readability refactor.

- Add MultiAjaxController for the `multi` bulk-operations action. The api.php
  block was an 8-level-deep if/else pyramid; it is now a thin switch($type)
  dispatcher over per-entity private handlers (handleLine/handleDevices/
  handleUser/handleServers/handleSeries/handleStreams), each a flat if/elseif
  chain answering via ok()/fail(). The repeated
  `implode(',', array_map('intval', …))` and connection-purge idioms are
  factored into inList()/purgeUserConnections()/purgeServerConnections().
  Behaviour and SQL preserved (verified: no new query/call introduced; only the
  intended de-duplication reduces occurrence counts).
- Register `multi` via $router->api(...) and remove the block from api.php.
  With every action now handled by a dedicated controller, api.php drops from
  4985 to 37 lines — a session-gated fallback returning {"result":false} for
  any action not peeled off by Router::dispatchApi() — and its ~40 now-unused
  imports are removed.

Known issue left untouched here (preserved from api.php, flagged in-code): the
mag/enigma permission map is keyed 'enigma2' while $type is 'enigma', so enigma
bulk ops resolve a null permission — to be addressed separately.

Verified: php -l, make gates, make phpstan (0), make cs.
2026-08-28 19:46:16 +03:00
Divarion_D 44367d06eb refactor(admin-api): extract the search action into a controller
Twelfth slice of the admin/api.php readability refactor — the single largest
block (the global fuzzy search across lines, MAG/Enigma2 devices, users,
streams and series, each rendered as an HTML result card).

- Add SearchAjaxController. Because the block is dominated by large literal
  HTML string-building, the body was lifted from api.php byte-for-byte (verified
  identical to the original, whitespace-insensitive) rather than retyped, then
  its 53 empty-then `if (c) {} else {…}` idioms were flattened to `if (!c) {…}`
  by a mechanical, negation-preserving pass (each inversion reviewed). HTML and
  control flow are otherwise unchanged; $language / $rSearchStatusArray are the
  legacy bootstrap globals it relies on.
- Register `search` via $router->api(...) and delete its block from api.php
  (1300 -> 452 lines; 4533 removed across the twelve slices).

Only the `multi` bulk-operations block now remains in api.php.

Verified: php -l, make gates, make phpstan (0), make cs.
2026-08-28 19:28:49 +03:00
Divarion_D 1b8e7fda1a refactor(admin-api): extract Providers and remaining misc actions
Eleventh slice of the admin/api.php readability refactor.

- Add ProviderAjaxController: provider (delete/reload), provider_streams
  (DataTables listing; gates inline since its permission-denied path uses the
  DataTables envelope, not {"result"}).
- Add MiscAjaxController for the leftover small actions that fit no larger
  domain: process, profile, watch_output, reguserlist, userlist, listdir,
  queue, delete_recording, clear_failures. Nested empty-then cascades
  flattened; OR-permission gates use gateAny(); comments English.
- Register all 11 actions via $router->api(...) and delete their blocks from
  api.php (1587 -> 1300 lines; 3685 removed across the eleven slices).

Only the two large blocks, search and multi, now remain in api.php.

Verified: php -l, make gates, make phpstan (0), make cs.
2026-08-28 19:15:20 +03:00
Divarion_D 2ae9bce190 refactor(admin-api): extract Backups, Logs & Reports actions into a controller
Tenth slice of the admin/api.php readability refactor.

- Add BackupAjaxController with 4 actions: clear_logs, backup, report,
  download_panel_logs. Logic ported faithfully; nested empty-then / if-else
  cascades flattened; comments English; dead $rRet dropped.
- report is the sole direct-navigation action (opened via window.location, no
  XHR header), so report() does not call requireXhr(); it dispatches
  TableController in-process and converts the captured output to a CSV/JSON
  download from a shutdown hook — ported verbatim.
- clear_logs' 6-permission gate uses gateAny(); download_panel_logs keeps its
  (pre-existing) lack of a per-action gate.
- provider_streams (a provider-domain action interleaved in the range) is left
  in api.php.
- Register the 4 actions via $router->api(...) and delete their blocks from
  api.php (1787 -> 1587 lines; 3398 removed across the ten slices).

Verified: php -l, make gates, make phpstan (0), make cs.
2026-08-28 19:09:58 +03:00
Divarion_D c8d7dc1aa8 refactor(admin-api): extract Stats & Graphs actions into a controller
Ninth slice of the admin/api.php readability refactor.

- Add StatsAjaxController with 3 actions: graph_stats, stats, header_stats.
  Logic ported faithfully; the many nested empty-then cascades flattened;
  comments English; dead locals / unused foreach keys dropped.
- stats was the file's largest block (~242 lines): the dashboard aggregation
  with its $getFallbackUptime closure and single-server vs whole-fleet /
  redis vs non-redis branches. $rTotalConnections is now initialised to 0 up
  front so the redis path never reads it undefined.
- graph_stats keeps its (pre-existing) lack of a per-action permission gate.
- queue (a job-queue action) is left in api.php.
- Register the 3 actions via $router->api(...) and delete their blocks from
  api.php (2186 -> 1787 lines; 3198 removed across the nine slices).

Verified: php -l, make gates, make phpstan (0), make cs.
2026-08-28 19:04:24 +03:00
Divarion_D 6e40de339a refactor(admin-api): extract Packages, Bouquets & Groups actions into a controller
Eighth slice of the admin/api.php readability refactor.

- Add PackageAjaxController with 8 actions: package, code, hmac, group,
  bouquet, category, get_package, get_package_trial. Logic ported faithfully;
  nested empty-then cascades flattened; comments English.
- get_package / get_package_trial keep their (pre-existing) lack of a
  per-action permission gate; their identical bouquet-resolution loop is
  shared via collectPackageBouquets().
- Register the 8 actions via $router->api(...) and delete their blocks from
  api.php (2387 -> 2186 lines; 2799 removed across the eight slices).

Verified: php -l, make gates, make phpstan (0), make cs.
2026-08-28 18:54:13 +03:00
Divarion_D fbdc719404 refactor(admin-api): extract Streams & VOD actions into controllers
Seventh slice of the admin/api.php readability refactor — the Streams/VOD
domain, split into two controllers.

StreamAjaxController (content control): stream, movie, episode, series.
- movie and episode were byte-identical bar the permission key -> shared
  vodAction(); delete/kill/purge were identical across stream/movie/episode ->
  shared streamMutation(). start/stop/restart/force echo the raw ApiClient
  response, as api.php did.

StreamToolsAjaxController (tools/lists/reviews): review_selection,
review_bouquet, serieslist, streamlist, adaptivelist, titlesync, probe_stream,
check_stream, get_episode_ids.
- review_bouquet's four near-identical member-collection blocks fold into a
  collectBouquetItems() helper; the Select2 lists keep JSON_PARTIAL_OUTPUT_ON_ERROR;
  probe_stream/check_stream echo their raw HTML tables. Empty-then cascades
  flattened throughout; comments English.

process, provider and profile (interleaved but other domains) are left in
api.php. Register all 13 actions via $router->api(...) and delete their blocks
(3142 -> 2387 lines; 2598 removed across the seven slices).

Verified: php -l, make gates, make phpstan (0), make cs.
2026-08-28 18:45:40 +03:00
Divarion_D f6fad9818f refactor(admin-api): extract Users & Lines actions into a controller
Sixth slice of the admin/api.php readability refactor.

- Add UserAjaxController with 5 actions: line, line_activity, adjust_credits,
  reg_user, ticket. Logic ported faithfully; nested branches flattened;
  comments English.
- Add LineStateTrait for the enable/disable/ban/unban/kill sub-actions that
  were byte-identical across the line, mag and enigma blocks (they all act on
  a line's user_id). DeviceAjaxController now uses the trait instead of its own
  private copy; its now-unused imports were dropped. The trait carries
  @phpstan-require-extends plus abstract ok()/fail() stubs so both PHPStan and
  Intelephense resolve the never-returning helpers.
- process (process-monitor) and profile (transcode profile) were left in
  api.php — they are not user/line actions.
- Register the 5 actions via $router->api(...) and delete their blocks from
  api.php (3330 -> 3142 lines; 1843 removed across the six slices).

Verified: php -l, make gates, make phpstan (0), make cs.
2026-08-28 18:17:45 +03:00
Divarion_D 7d6b196f7a style(routes): normalize admin route section headers
Drop the cryptic "Group A/B/C…" prefixes from the page-route section headers,
keeping the descriptive name in the same boxed style, and render the ajax
API-action sub-sections (Cache & Handlers, Servers & Ops, Blocklists &
Security, Devices, EPG) in the same style. Also fixes one section comment that
was left in Russian.

Comments only — no route or behaviour changes.
2026-08-28 18:05:11 +03:00
Divarion_D ba1eaa8827 refactor(admin-api): extract EPG actions into a controller
Fifth slice of the admin/api.php readability refactor.

- Add EpgAjaxController with 8 actions: epg, epglist, force_epg,
  epg_auto_assign, epg_categories, provider_import_epg, get_epg, get_programme.
  Logic ported faithfully; nested empty-then cascades flattened; comments
  English; dead locals dropped ($rGroups, $rChannelsSort, $rBestProvNorm).
- Actions answering with a {"status": STATUS_*} envelope (epg_auto_assign,
  epg_categories, provider_import_epg) gate inline via Authorization::check(),
  since their permission-denied path also uses that envelope, not {"result"}.
- BaseAjaxController::json() gains an optional $flags argument so epglist can
  keep JSON_PARTIAL_OUTPUT_ON_ERROR.
- provider_streams (a provider-domain action interleaved in the range) is left
  in api.php.
- Register the 8 actions via $router->api(...) and delete their blocks from
  api.php (3750 -> 3330 lines; 1655 removed across the five slices).

Verified: php -l, make gates, make phpstan (0), make cs.
2026-08-28 18:04:45 +03:00
Divarion_D 2b271aee87 refactor(admin-api): extract Devices actions into a controller
Fourth slice of the admin/api.php readability refactor.

- Add DeviceAjaxController with 4 actions: mag, enigma, mag_event, send_event.
- mag and enigma were near-identical; their shared line-state sub-actions
  (enable/disable/ban/unban/kill, all keyed on the device's owning line
  user_id) are factored into a private lineStateAction() helper, leaving only
  the differing delete/convert calls in each method.
- send_event: the nested type if-else cascade flattened to an elseif chain.
- Register the 4 actions via $router->api(...) and delete their blocks from
  api.php (3966 -> 3750 lines; 1235 removed across the four slices).

Verified: php -l, make gates, make phpstan (0), make cs.
2026-08-28 17:53:13 +03:00
Divarion_D 905318ef47 refactor(admin-api): extract Blocklists & Security actions into a controller
Third slice of the admin/api.php readability refactor.

- Add BlocklistAjaxController with 7 actions: useragent, isp, mysql_syslog, ip
  (blocklist management), ip_whois (GeoIP/ISP/ASN lookup — no per-action gate,
  as in api.php), asn (block/allow ASN), decrypt_text (stream-token decrypt).
  Logic ported faithfully; empty-then conditionals flattened; comments English.
- ip_whois: silence the pre-existing undefined-key warning on the optional
  `isp` sub-array via !empty() (behaviour unchanged — the ASN branch only ran
  with isp data present).
- Register the 7 actions via $router->api(...) and delete their blocks from
  api.php (4199 -> 3966 lines; 1019 removed across the three slices).

Verified: php -l, make gates, make phpstan (0), make cs.
2026-08-28 17:47:45 +03:00
Divarion_D 0ba2241211 refactor(admin-api): flatten empty-then conditionals in ServerAjaxController
Simplify the ported `if (cond) {} else { body }` idioms (empty then-branch)
into positive form `if (!cond) { body }`, invert the fingerprint validation
guard and de-indent its body, and fold `if (c) {$e=true;} else {$e=false;}`
into `$e = (c);`. Also drop two dead assignments ($rSuccess, the unused catch
binding $rE). All changes are behaviour-preserving.

Verified: php -l, make phpstan (0), make cs.
2026-08-28 17:42:28 +03:00
Divarion_D 8d4693d6f4 refactor(admin-api): extract Servers/Ops actions; move ajax controllers to Admin\Ajax
Second slice of the admin/api.php readability refactor, plus housekeeping.

- Relocate the extracted ajax controllers into a dedicated
  XcVm\Public\Controllers\Admin\Ajax namespace/folder (BaseAjaxController,
  CacheAjaxController moved here from the previous slice).
- Rewrite the ajax-controller docblocks/comments in English (repo standard
  going forward).
- Add BaseAjaxController::gateAny([[type,key],...]) for the OR-permission
  idiom `if (check(a) || check(b))` used by the server-view family.
- Add ServerAjaxController with the 17 Servers/Ops actions: rtmp_ip,
  rollback_versions, server, proxy, fingerprint, restart_all_services,
  restart_services, reboot_server, update_binaries, server_view, server_stats,
  rtmp_kill, install_status, reinstall_server, fpm_status, update_all_servers,
  update_all_binaries. Block logic ported verbatim; the repeated server_id
  normalize idiom folded into a private helper.
- Register the 17 actions via $router->api(...) in routes/admin.php and delete
  their blocks from api.php (4825 -> 4199 lines; 786 removed across both slices).

Two deliberate departures from a byte-for-byte port, both documented in the
controller:
- rtmp_kill echoes the raw ApiClient::systemRequest() response (not JSON), as
  api.php did.
- proxy->kill previously read an undefined $rServerID (a latent bug:
  getAll()[null] -> no-op + warnings in redis-handler mode); it now uses
  server_id so the redis path actually closes the proxy's connections. This is
  a behaviour change on that path — flagged for review.

Verified: php -l, make gates (incl. check-procedural-use), make phpstan (0),
make cs. Actions still gated identically and reached via Router::dispatchApi()
before the AjaxController fallback.
2026-08-28 17:20:13 +03:00
Divarion_D 5c5ae46a2c refactor(admin-api): extract Cache & Handlers actions into a controller
First slice of the admin/api.php readability refactor. api.php is a ~4985-line
flat chain of `if (action == 'x') { … exit(); }` blocks with heavy boilerplate
(≈96 permission-gate/else-fail pairs, ≈292 exit()s). Actions are peeled off one
group at a time via Router::dispatchApi(), which runs before the AjaxController
→ api.php fallback, so migrated actions leave api.php untouched for the rest.

- Add BaseAjaxController: JSON-only base (POPO, like TmdbController) providing
  ok()/fail()/gate()/requireXhr()/json(), collapsing the repeated
  Authorization::check(){}else{json false;exit} obvious into `gate()`. The XHR
  guard from api.php's top is replicated per-action so behaviour is identical.
- Add CacheAjaxController with the 6 Cache & Handlers actions (regenerate_cache,
  enable_cache, disable_cache, enable_handler, disable_handler, clear_redis).
  Block logic ported verbatim — no behaviour change.
- Register the 6 actions via $router->api(...) in routes/admin.php and remove
  the corresponding blocks from api.php (4985 → 4825 lines).
- Annotate $router with @var in routes/admin.php to resolve the pre-existing
  "Undefined variable $router" IDE warning (FQN, no use-import).

Verified: php -l, make gates (incl. check-procedural-use), make phpstan (0),
make cs. New classes autoload via PSR-4 (no dump needed).
2026-08-28 17:04:04 +03:00
Divarion_D 3a3e506be2 fix: guard undefined-key/null warnings and missing certbot logs dir
Surfaced in the panel error log:

- api.php: the server 'delete' action read $rServers[server_id]['is_main']
  without an isset() guard, so deleting an id absent from $rServers (already
  removed / orphan) raised "Undefined array key" + null offset access. Mirror
  the isset() guard already used at the 'restart' action.
- post.php: the server-save SSL regeneration path read $rData['domain_name']
  in is_array()/explode() with no key check, warning twice when the field was
  omitted. Normalise a missing key to an empty array.
- CertbotCommand: file_put_contents() wrote the run log into certbot/logs/,
  which certbot only creates via --logs-dir when it actually runs; with no
  domains (or on a fresh server) the dir was absent and the write failed.
  mkdir it before writing, ahead of the chown so ownership is fixed too.
2026-08-28 16:28:50 +03:00
Divarion_D 25e2adfcac Prepare release 2.4.2 2.4.2 2026-08-27 18:15:50 +03:00
Divarion_D d4da90f37b fix(docs): translate bold spans atomically and auto-prune orphaned ru pages
The line-by-line web translator reordered words inside `**bold**` spans and
misplaced/dropped the markers, producing `**LB` or `****` (empty bold). Mask
each `**...**` as ONE atomic sentinel: translate the inner text on its own,
then store the whole balanced `**inner**` — the engine never sees the markers
and cannot reorder or collapse them. Also harden the anthropic prompt to keep
emphasis balanced.

Auto-prune: after translating, delete generated docs/ru files whose docs/en
source no longer exists (renamed/removed) and drop now-empty dirs, so the tree
mirrors docs/en 1:1 (removes the stale development/modules.md and
guides/geoip-and-device-detection.md).

Bump PROMPT_VERSION to 6 to invalidate the contaminated cache and regenerate
docs/ru (0 broken bold spans remaining, aside from pre-existing multi-line
bold that spans a soft line break).
2026-08-27 18:07:43 +03:00
Divarion_D be2c4b3ad7 docs: reorder release checklist, run make new first, fold docs build into validation
- Move "Pre-Release Validation" to step 2 (right after Changelog) and fold the
  "Regenerate translated documentation" sub-step (make docs-translate/build)
  into it, so code checks and doc regeneration happen together, early.
- Run `make new` as the very first action (step 1), before generating
  dist/changes.md, and drop it from "Build Archives". `make new` wipes AND
  recreates dist/, so running it in the build step deleted the changes.md
  generated in step 1; main/lb don't depend on new and their final `clean`
  only removes TEMP_DIR, so changes.md now survives to the GitHub Release step.
- Renumber sections and update the make-new command reference.
2026-08-27 18:07:42 +03:00
Divarion_D e50e15741c fix(admin): initialize $rKeyCount on the live-connections refresh path
handleLiveConnections() set $rKeyCount in every branch except the
`refresh` request path: there $rFilterBefore stays true, so the
`if (!$rFilterBefore) $rKeyCount = count($rKeys)` fallback is skipped and
the later `$rKeyCount--` in the mGet loop hit an undefined variable
(WARNING: Undefined variable $rKeyCount at TableController.php:2741).

Initialize $rKeyCount = count($rKeys) in the refresh branch — the count
starts at the number of requested keys and is decremented for dead/filtered
rows, matching the non-filter-before path. (Public/ is index-only for
PHPStan, so this was never surfaced by static analysis.)
2026-08-27 16:28:46 +03:00
Divarion_D 08a3c69e0b refactor: drop redundant require_once for PSR-4-autoloaded classes
Audited every require/include in src/. Removed 45 manual require_once/require
statements that load a XcVm\* class in a context where the Composer autoloader
is already registered — the class resolves on first use, so the require is dead:

- CronTrait ×26 (Cli/CronJobs/*), DaemonTrait ×7 (Cli/Commands/*)
- DropboxClient ×5 (Core/Backup/BackupService)
- XmlStringStreamer (EpgCronJob; was a bare `require`, a redeclare risk)
- Thread + Multithread (CacheEngineCronJob)
- Logger, DatabaseHandler, LegacyInitializer, Translator (bootstrap.php — the
  autoloader is registered earlier at line 89; these are also `use`-imported)

Deliberately KEPT (not redundant):
- The lightweight WebApiBootstrap / StreamingRequestBootstrap / StreamingBootstrap
  have NO Composer autoloader (intentional, for high-traffic endpoints) — their
  manual requires are load-bearing.
- ErrorHandler.php (global namespace, not PSR-4), and the define()-only config
  files AppConfig.php / Paths.php / Binaries.php (autoload never loads constants).
- All view/template/procedural/dynamic includes.

Every removed target verified to resolve via the production autoloader
(class_exists / trait_exists). make phpstan / cs / gates all green.
2026-08-27 16:20:09 +03:00
Divarion_D 72eb0fbc58 refactor(bootstrap): replace per-scope include pairs with ScopeBootstrap classes
The front controller selected a pair of procedural files per scope
(<scope>_session.php + <scope>_functions.php) from a hardcoded map and
require'd them at global scope. Consolidate into a typed class hierarchy:

- ScopeBootstrap (interface) + ScopeBootstrapFactory::create($scope)
- Admin/Reseller/PlayerScopeBootstrap, each porting its former session +
  functions logic 1:1 into boot()/bootSession()/bootFunctions().

index.php now calls ScopeBootstrapFactory::create($scope)->boot() instead
of building file paths and require'ing. Unknown scopes still fall back to
admin.

Behaviour is unchanged: the view-facing globals ($rUserInfo, $rPermissions,
$_STATUS, $customScript, $_PAGE, server/health flags) are still injected via
explicit `global` declarations in the boot methods, so the ~140 procedural
view templates read them from scope exactly as before. player_utility_-
functions.php is unchanged and still loaded by PlayerScopeBootstrap.

Also fix the PHPStan stub: SERVER_ID is an int (getMainID(): ?int, a
server_id PK), not a string. Moving its define() into a class method meant
PHPStan no longer inferred the type from a top-level define and fell back to
the stub, which wrongly typed it `(string) mt_rand()` — surfacing 20
false argument.type errors at int-param call sites. Stub now uses mt_rand().

Removes: admin_session_fc.php, admin_functions_fc.php, reseller_session.php,
reseller_functions.php, player_session.php, player_functions.php.

make phpstan / cs / gates all green.
2026-08-27 15:57:35 +03:00
Divarion_D 512a7a2985 fix: resolve all PHPStan errors and remove the frozen baseline
Drop build/phpstan-baseline.neon (122 frozen pre-existing errors) and fix
the underlying issues in source instead of suppressing them. A fresh
level-5 run without the baseline surfaced 158 errors across 56 files; all
are resolved. `make phpstan`, `make cs`, and `make gates` are green.

Fix categories:
- variable.undefined — initialize vars to a correct default before the
  branch/loop that conditionally set them, so every path defines them.
- return.type — align declared return types with reality (widen to
  array|false / ?array / \Movie|null etc. where callers handle the
  sentinel, or return the declared type consistently).
- argument.type — cast at call sites (curl_setopt/stream_set_blocking bool
  flags, str_replace/mktime/uniqid operands, \CurlHandle phpdoc).
- redundant/dead conditions — simplify always-true guards and drop
  unreachable else branches, preserving behavior.

Real latent bugs caught along the way:
- Core/Http/CurlClient — retry loop never incremented on failure and never
  broke on success (could spin); now retries as documented.
- Core/Util/StreamUtils — explode('=', $x, 1) meant path/domain query
  params were never parsed (limit 1 -> 2).
- Cli/CronJobs/RootSignalsCronJob — set_governor emitted an undefined PHP
  $i into the cpufreq-set bash command (now a literal shell $i).

Root-cause analyzer fixes (not suppressions):
- Database::ping()/num_rows() marked @phpstan-impure so repeated calls are
  not treated as constant (clears the ResellerApiDispatcher false cluster).
- StreamService::getArchive() $rReturn given an explicit @var for the
  loop-accumulated shape PHPStan cannot infer from array().

Two residual entries are documented false positives (analyzer limitations,
not bugs) as path-scoped ignoreErrors in phpstan.dist.neon:
- CacheHandlerCommand:66 — settings force-reloaded from DB mid-run.
- EpgCronJob:314 — reconnect-verify ping() after db_connect().

Also drops an unused `use ...Epg\EPG;` import in admin/api.php.
2026-08-27 15:24:41 +03:00
Divarion_D 6874ba3f90 fix(fanout): move keepalive log out of cron-swept tmp/
cron:tmp (TmpCronJob) deletes every file directly in tmp/ older than 600s,
so the continuously-running fanout daemon's tmp/xc_fanout.log was unlinked
out from under it — the daemon kept writing to the orphaned inode (log
invisible, space held until restart).

Write to bin/xc_fanout/xc_fanout.log (never swept) and truncate (>) per
daemon generation so it stays bounded — same convention as the PHP daemons'
tmp/daemon_<name>.log.
2026-08-27 12:46:07 +03:00
Divarion_D 5a5d1ad8c3 fix: guard undefined-key/null warnings surfaced in panel error log
Harden the hot paths that dominated the panel error log with defensive
null/undefined-key guards; no behaviour change on the happy path:

- ConnectionTracker::removeFromQueue — skip when the queue signal file is
  absent instead of file_get_contents() on a missing path (219 warnings)
- ConnectionTracker::closeConnection — fill server_id/pid/activity_id/etc.
  defaults on partial connection arrays before comparisons
- ShutdownHandler — empty()-guard redis_handler so a null settings cache
  no longer throws "array offset on null"
- SeriesService — coerce json_decode(bouquet/category_create_list) to array
  before foreach (create + update variants)
- PlayerApiController — coerce movie_properties to array and ?? its keys
- TableController — ?? "" on movie_image before strlen (2 sites)
- admin/api.php — init $rInfo codec defaults for audio-less streams and
  ?? 0 the iostat avg-cpu chains
2026-08-27 12:46:06 +03:00
Divarion_D c982bb2f1e docs: audit dev docs for source drift, split oversized pages, add core-wiring
Verify every dev-doc claim against src/ and fix factual drift: wrong method
signatures/return types, wrong enum casing (BootContext cases are PascalCase),
stale paths (M3u parsers are Composer deps under vendor/, MobileDetect is
mobiledetect/mobiledetectlib v4.9.0 \Detection\MobileDetect, NotFoundException
lives in XcVm\Core\Container\Psr), a fictional `stream:check` command/class,
reversed migration-failure semantics ([FAIL] = not recorded, retried),
inverted isStreamRunning/isStreamAlive descriptions, findProcessPIDs ANY-not-ALL,
acquireCronLock has no shutdown callback, and nonexistent make targets.

Split oversized pages and fix nav + cross-links:
- modules.md -> module-authoring / module-lifecycle / module-extension-points
- cli-tools.md -> cli-tools + database-migrations
- streaming-subsystem.md -> + streaming-diagnostics
- geoip-and-device-detection.md -> geoip-isp-and-geo-routing + device-detection-and-stb-locking

Add development/core-wiring.md: how the core assembles itself at boot
(container population, ServiceContainer reference, bootAll orchestration,
CLI command auto-discovery, end-to-end Admin/CLI boot walkthroughs).

Only docs/en + mkdocs.yml touched; docs/ru is regenerated before release.
2026-08-26 22:42:43 +03:00
Divarion_D 5ce1671ef0 fix(streaming): dedup HLS connections by player, not per-request uuid
HLS viewers were over-tracked, especially on channel switch: each playlist
request through auth.php minted a fresh md5(uniqid()) uuid, and HLS
connections are keyed by uuid — so every (re)select created a NEW tracked
connection that the 30s/60s reaper only cleared slowly. A single viewer
switching channels ballooned to ~19 live connections and tripped
max-connections limits. Verified live on the test server.

Derive the HLS connection id from line identity + stream + client IP +
user-agent instead (new ConnectionTracker::hlsConnectionKey()), overriding
the token uuid for the m3u8 arm in live.php before the connection context is
built — so tracking, segment auth and the heartbeat file stay consistent.
The same player re-requesting the same channel from the same IP now reuses
ONE connection; different stream, IP or user-agent stay distinct — preserving
multiple streams from one IP and multi-device counting. TS/VOD keep their
per-request random uuid.

Verified: same player x4 -> 1 connection; different UA -> separate;
different stream -> separate.
2026-08-26 20:22:00 +03:00
Divarion_D 55f6fcf206 feat(update): add per-server version rollback from the panel
Adds a "safety net" rollback so a server can be downgraded to an earlier
release without a manual redeploy, mirroring the existing update flow.

- GitHubReleases: getVersionFile() fetches a specific version's asset (not
  just latest); getPreviousVersions() lists the newest releases older than a
  given version, each flagged beta (GitHub prerelease). getReleases() now
  delegates to a shared getFilteredReleases() that keeps the prerelease flag.
- UpdateCommand: new `rollback <version>` case — validates the target is a
  real, strictly-older release; on MAIN takes an automatic DB backup first
  (aborting if it fails); downloads the exact version (main/lb_update asset),
  verifies MD5, and hands off to the same python `update` applier.
- RootSignalsCronJob: dispatch the `rollback` signal to `console.php update
  rollback <version>` (version regex-validated, escapeshellarg'd).
- api.php: `rollback_versions` (channel-aware list, optional per-server base
  version) and server `sub=rollback` (validates version, writes the signal).
- servers.php: per-server "Rollback Version" action (dropdown item + button)
  opening a modal to pick a version; beta builds show "(beta)". Works for
  MAIN and each LB independently.
- docs: rollback sections in server-update.md and update-system.md.

The downgrade reuses the version-agnostic python applier, so binaries,
config and user data are preserved. Migrations are forward-only and are not
rolled back; the automatic MAIN backup is the recovery path.
2026-08-26 19:27:29 +03:00
Divarion_D f0e387d238 fix(streaming): apply HTTP reconnect to all live sources, not just LLOD
Regular live HTTP(S) streams got no reconnect resilience — only the LLOD
path did. A flaky upstream that briefly drops the connection makes ffmpeg
exit ("Stream ends prematurely"), and the watchdog then kills and restarts
the whole stream, so marginal sources flap (start "not on the first try,
sometimes never"). Verified live: stream 560 (a slow XC_VM source) ran
ffmpeg at 0.5x with the HLS playlist stalling ~100s before each watchdog
restart, while a fast-CDN stream on the identical pipeline stayed up 21h.

Broaden the reconnect guard from `$rLLOD && ...` to every non-loopback
http(s) source: `-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5`.
Keep `-fflags +discardcorrupt` scoped to the on-demand (LLOD) path where it
originally shipped. Flags stay before `-i` (http-demuxer input options) and
are still guarded to http(s) — they are a fatal "Option not found" on
udp/rtmp/file inputs.
2026-08-26 18:26:48 +03:00
Divarion_D 54bb2659fd fix(admin): repair CSV export and add JSON export to listing pages
Export as CSV produced an empty download: the report action fetched its
rows over a loopback HTTP call to 127.0.0.1:<http_broadcast_port>/table,
which is unreachable under the Front Controller, and the button navigates
via window.location so the api.php XHR guard exit()ed the request in prod.

- api.php: exempt the `report` download action from the X-Requested-With
  guard (still gated by session + `backups` permission), and replace the
  dead loopback curl with in-process TableController dispatch (as in
  AdminAPIWrapper::TableAPI), capturing its JSON via a shutdown hook and
  converting to CSV or JSON per the new `format` param.
- topbar.php: add an "Export as JSON" button next to every "Export as CSV"
  entry and widen the permission gate to cover both.
- listings.js: add a delegated #btn-export-json handler that exports the
  visible ./table DataTable (current filters/sort) as JSON.
2026-08-26 17:14:54 +03:00
Divarion_D d861844ba2 fix(ministra): gate client STB-type/firmware cut_off behind flags (off, as in 2.3.9)
Porting the portal to Ministra 5.6.10 (re-)enabled two client-side cut_off() gates the XC_VM fork had kept off in 2.3.9: the STB-type gate (commented out in 2.3.9) and a brand-new 'outdated_firmware' gate. Both reject legit boxes ("your set-top box is not supported" / "outdated_firmware") even though Allowed STB Types is enforced server-side (portal.php get_profile / $rVerified) and the backend sends no autoupdate data — which is exactly why the 2.3.9 xpcom.common.js works as a drop-in while 2.4.1 does not. Expose both as top-level flags (XCVM_ENFORCE_STB_TYPE / XCVM_ENFORCE_FIRMWARE, default false) and guard the two branches with them, restoring 2.3.9 behaviour while keeping the upstream code reachable by flipping a flag. A full 2.3.9-vs-2.4.0 diff confirmed these are the only box-rejection gates that differ (init/load/handshake/get_server_params are otherwise identical bar formatting).
2026-08-25 22:04:24 +03:00
Divarion_D ae8526af0d fix(ministra): stop STB redirect loop on server/load.php under a code path
A Stalker STB pings <portal_base>/server/load.php (load-balancer handshake) and may follow to server/login. Under an access-code portal path (/CODE/server/load.php) these have no file and no route, so the front controller falls to the admin-session fallback (ministra is an unknown scope), whose relative ./login redirect resolves back onto the same /CODE/server/... path and 302-loops forever — the box hangs on authorization. The nginx shim only catches the root /server/load.php (the ^~ /CODE/ code block preempts regex locations), so handle the code-path case in index.php: for the ministra scope, redirect server/load.php|server/login to portal.php in the SAME base (query preserved), which answers the handshake. Verified live: /CODE/server/load.php now serves handshake/get_profile/do_auth and the box browses VOD; normal admin login redirects unchanged.
2026-08-25 22:04:10 +03:00
Divarion_D f17792b207 fix: harden Redis teardown and guard missing hmac_id in live connections
- RedisManager::close(): swallow exceptions from $rRedis->close(). A half-dead socket throws RedisException ('read error on connection') on close, which propagated through reconnect() -> ConnectionTracker::getCapacity() and killed the watchdog daemon. Teardown must never fatal — the instance is discarded either way and reconnect() opens a fresh one.

- TableController: guard $rRow['hmac_id'] with !empty() in both live-connections renderers (lines 2567 and 2891). Non-HMAC connections have no hmac_id key, raising 'Undefined array key' on every render.
2026-08-25 19:06:02 +03:00