Add a push-to-main workflow that parses issue references in the
pushed commit messages and moves each referenced issue's card on the
org Projects v2 board (Vateron-Media/projects/5) to the
"Review & Deploy" status column, without closing the issue.
Reuses the existing ADD_TO_PROJECT_PAT secret.
The bundled ffmpeg/ffprobe 8.0 has a hard NEEDED dependency on
libogg.so.0. The debian13, ubuntu20 and ubuntu24 package lists (in
both the MAIN installer and the LB install flow) omitted libogg0,
so ffmpeg would fail to start with "error while loading shared
libraries: libogg.so.0" on those distributions.
Add libogg0 next to libnuma1 in all three lists, in both
install (PACKAGES) and LbInstallFlow::getPackages(), keeping MAIN
and LB in sync. redhat already ships the equivalent libogg RPM.
Refs: #152
AdminAPIWrapper::TableAPI proxied over HTTP to `<code>/table.php` on the
broadcast port to reuse TableController's DataTables logic. That round-trip is
dead under the Front Controller, so every list action (get_lines, get_users,
get_streams, get_mags, …) returned literal `null`:
- the URL was built from `dirname($_SERVER['PHP_SELF'])`, which now resolves to
`/public` (nginx sets SCRIPT_NAME=/public/index.php), not the access code;
- nginx.conf 404s every `*.php` (`location ~ \.php$ { return 404; }`);
- the api-scope access code routes every path back to AdminApiController, never
TableController — so even a corrected URL just loops to "Invalid action".
`json_decode(curl_exec(...))` therefore always produced null.
Dispatch the handler in-process instead: seed RequestManager with the request
params and call TableController::index(), which authenticates via api_key and
echoes the JSON directly. Safe because XC_Bootstrap::boot() is idempotent and
TableController has no constructor deps; all TableAPI call sites are passthrough
`echo json_encode(...)`. Validated live against get_lines/users/streams/mags/
movies/stations plus show_columns filtering.
Drop the two ministra restoration working-notes now that the work is
done: ministra-unused-modules.md (dead-module audit complete) and
ministra-5.6.10-lost-customizations.md (all items resolved). Also drop
the now-dangling doc reference from the get_modules debug comment.
The ministra-browser-emulation.md integration guide is kept.
Review of the remaining 5.6.10 lost-customization items:
- player.js: multi-audio (titles/infoCurtitle) kept and improved by
5.6.10; xc_vm PVR branch intact; parental flow lives in tv.js, not
here. No changes needed.
- account.js: keep the full 5.6.10 account screen (decision) instead of
restoring the minimal Phone+message fork screen.
- xpcom.common.js: allowed_stb_types/aurahd richer in 5.6.10;
outdated_firmware (player<1382) only gates non-whitelisted types and
is more permissive, not a regression; check_image_version is a no-op
(backend sends no autoupdate); load_channels parental left to 5.6.10 +
tv.js guards to avoid a double password prompt.
Re-apply two xc_vm customizations that the 5.6.10 client drop wiped out:
- Channel logos (tv.js handling_block + player.js preview): on xc_vm the
backend sends a ready-to-use logo URL, but 5.6.10 rebuilt a classic
stalker misc/logos/<size>/ path, breaking the image. Use the URL
directly; keep the 5.6.10 timeshift_mode class on the player logo.
- TV color buttons (tv.js): in the xc_vm theme the 4th button is channel
search (search_menu_switcher) instead of the fav-manage "move" mode.
Hybrid with the new 5.6.10 quality-filter button — quality wins when
the profile enables tv_quality_filter, otherwise search.
Also drop the obsolete snumber/sortIDs note: the backend now numbers
channels sequentially, so 5.6.10 sortBy("number") is correct.
- Add ministra-5.6.10-lost-customizations.md: what our fork's clean
5.6.10 drop wiped out and how to restore it (get_types_list, tv.js
parental control, channel logos, account screen, DVR .ts format, ...).
- Update ministra-browser-emulation.md for the ?debug_key gate.
Bring xpcom.common.js, player.js, account.js, time_shift.js, version.js
and tv.js up to Ministra 5.6.10 while re-applying our fork's changes
that the clean upstream drop wiped out:
- xpcom.common.js: portal.php API endpoint + portal_path regex,
get_types_list wiring, browser debug/emulation gate (?debug_key),
layer.sclub_info in base_modules.
- tv.js: restore parental-control guards (genre.alias != "for adults")
in the EPG, full-screen and preview paths. 5.6.10 switched the guard
to genre.censored and added a home-genre lookup defaulting to
censored=1, which prompted for a password on every channel open.
- time_shift.js: keep .ts segment format (flussonic-style DVR).
- account.js: keep the minimal account screen.
Remaining restoration items tracked in
docs/ru/guides/ministra-5.6.10-lost-customizations.md.
- Add stb/get_types_list action: the 5.6.10 client fetches the allowed
STB-type whitelist via a dedicated call and stores it in
stb.allowed_stb_types (the field the client-side stb_type check reads,
separate from the profile copy). Without it the response was empty and
every device read as "not supported".
- Filter the get_preload_images list by is_file() so themes on sprites
no longer 404 on removed per-module icons.
- Drop server handlers for deleted modules (weatherco, course,
get_demo_video_parts) and their orphaned language keys / demo_video
profile field in portal.php.
- Guard MAGSCAN serial/device checks behind !$rDebug so browser
emulation (debug_key) can reach the portal.
Drop portal JS modules that the loader never pulls in (not in
base_modules/all_modules, no dynamic loader, no live references) and
their theme assets:
- infoportal branch: infoportal, anecdote, cityinfo, horoscope,
weather.current, weather.day, course.cbr, course.nbu, game.mastermind
(root module dead + crashes main menu when force-enabled)
- magiccast (leaked proxy creds to an external Infomir service)
- youtube (web wrapper dir absent from repo)
- demo, service_management (stub features, no server handlers)
- karaoke (no backend; "radio only")
- pvr.js (legacy Pvr, never instantiated; live local PVR is
pvr_local/records)
- JsHttpRequest-debug.js (core loads JsHttpRequest.js)
Removes 15 per-theme CSS variants + menu icons per module. The dead-code
audit is tracked in docs/ru/guides/ministra-unused-modules.md.
Update external/settings (the STB device-settings iframe) to 5.6.10:
refreshed the 31 HTML pages, 6 CSS files and language files. This adds
support for newer STB models (MAG520/522/524/526/540, im44xx) and 4K
video output modes, plus the "576-50" -> "576p-50" mode-label fix and new
"Time format" strings. The main.js/pres.js engine already matched 5.6.10.
Rewrite the 5.6.10 script paths ../../c/*.js -> ../../*.js to match our
flattened src/Ministra layout (no c/ subdir). Note: this replaces our
previous custom settings skin with the 5.6.10 stock styling; verify the
settings screens on a device.
Port the 5.6.10 null-safety in the TV layer's hide(): guard
password_input before dereferencing .on (avoids a TypeError if it is
not yet created) and also close parent_password_promt on hide, so the
parental-control prompt is dismissed together with the TV layer.
Other 5.6.10 additions to the kept-ours core files were evaluated and
skipped as non-applicable or too risky: multi-audio/title support and
the STB model list already exist here, timezones are server-driven
(we don't use the client timezone_list), and the #33139 seek fix
would require editing the deeply-diverged player.js at 3 call sites.
Overlay the 5.6.10 cappuccino/default/digital/emerald/ocean_blue themes
(413 CSS reformatted to house style, 6 updated sprites/previews, plus
course.widget CSS). template/xc_vm (our own design) is left untouched.
Rename password_continer -> password_container in password_input.js and the
xc_vm theme CSS so our client pairs with 5.6.10's fixed stock CSS class.
The 5.6.10 RTL .otf fonts + fonts.css are intentionally omitted: our
index.html does not load the fonts module, so the client already renders
with fallback fonts (as it always has); shipping them would be ~14 MB of
dead weight.
Rebase our xc_vm customizations onto the 5.6.10 client scripts. Of the 72
common files, 39 already matched 5.6.10; 14 adopted verbatim (incl.
vclub.js movie-lock/password + HD filters and a settings.js query-string
fix); 4 merged per-hunk keeping our xc_vm design branches (global,
main_menu, layer.list, layer.base).
5 deeply-diverged core files (xpcom.common, player, tv, account,
time_shift) and index.html are intentionally left as-ours to preserve the
debug-mode (?mac=), parental control, .ts DVR and the custom account
screen; a cherry-pick list of the forgone 5.6.10 features is tracked
separately.
MigrationRunner split each .sql file into statements on `;` BEFORE
removing full-line `--` comments, so a semicolon inside a comment (e.g.
010_maxmind's "no GeoIP data; on the old schedule…") broke parsing:
the trailing comment fragment leaked onto the next statement, producing
invalid SQL that failed forever ([FAIL] not recorded — will retry).
Strip comment lines from the whole file first, then split on `;`.
The installer downloads the GeoLite2 databases by running
`console.php cron:maxmind --force`, but that step is non-fatal and its exit
code is unchecked, so a transient network/GitHub failure at install time
leaves the panel with no GeoIP data and no error. Recovery then waited for the
next Tuesday because both the crontab schedule (0 4 * * 2) and the command's
internal gate restrict it to Tuesdays — meanwhile ministra/portal.php fatals on
the absent GeoLite2-Country.mmdb.
- MaxMindCronJob: when GeoLite2-Country.mmdb is missing, run regardless of the
Tuesday gate. The download steps already fetch only the files that are
actually absent, so a present database is still skipped.
- migration 010: move the `maxmind` crontab entry from Tuesday-only to daily so
the self-heal can trigger within a day of a failed install download. The real
weekly refresh still only happens on Tuesdays via the command's own gate. The
UPDATE is guarded to the old default so a customised schedule is preserved.
Run prettier over the whole src/Ministra tree (js/css/html/php) to normalise
to the project house style (tabs, LF, double quotes, printWidth 100). Pure
formatting, no behavioural change — this is the clean baseline so the upcoming
Ministra 5.6.10 client update diffs by content only, not whitespace.
Adds a `make phpstan-deadcode` target and build/phpstan-deadcode.neon that
layer tomasvotruba/unused-public on top of the main PHPStan setup to report
unused PUBLIC methods/properties/constants. It is an on-demand audit, NOT a
CI gate (expect false positives for dynamically-invoked code — routes,
#[ListensTo] subscribers, CLI handlers, view templates).
unused-public is a require-dev package (installed by `make dev-tools`); the
committed vendor/ stays production-only.
Add an entry (RU + EN) explaining why a MAG/STB box gets its IP blocked
after a factory reset / firmware change: the portal's MAGSCAN anti-clone
check compares the posted serial against the stored mag_devices.sn (and
device_id/device_id2/hw_version when lock_device is on) and bans the IP on
mismatch (blocked_ips -> iptables). Documents the fix — reset the device's
stored sn/device_id in the panel — and how to unblock: the web panel
(Tools -> IP Management, /<admin-code>/ips), console.php tools flush, or
manual iptables + flood-marker removal.
Ministra stops being a module — the whole Stalker portal (portal.php,
MinistraBootstrap, PortalHandler/PortalHelpers and the STB front-end) now
lives in src/Ministra/ under the XcVm\Ministra namespace, served at
/home/xc_vm/Ministra via the nginx alias.
- src/ministra/* and Modules/ministra_85a7d/{PortalHandler,PortalHelpers}
→ src/Ministra/; MinistraModule.php + module.json removed. Ministra was
the only committed module, so src/Modules/ keeps a .gitkeep.
- portal.php resolves PortalHandler as a sibling and derives MAIN_HOME from
its new location (glob crutch gone).
- nginx alias + AuthRepository $rAlias switched to /home/xc_vm/Ministra
(PascalCase); ministra entry dropped from bundled_modules.php.
- Makefile: Modules/ removed from LB_DIRS — all modules are MAIN-only, so
the ~50 MB of portal assets no longer ship to LB nodes.
- ArchitectureTest: zero committed modules is now a valid state.
- PHPStan: analyse src/Ministra, exclude the procedural portal.php entry,
repath the ministra baseline entries.
- Docs (architecture, ministra-browser-emulation, extraction plan) updated
to the new layout; the "extract to a separate repo" plan is cancelled.
Verified: php -l, make gates, make phpstan (No errors), full unit suite
(432 tests). On-server smoke: handshake + get_profile work end-to-end with
a registered MAC after deploy.
BoundaryInterface was a marker interface with no runtime consumer —
nothing read getEntryPoint()/isIsolated() and, being static-less
metadata, it enforced nothing. Its only implementor was MinistraModule.
Removes the interface, drops `implements BoundaryInterface` plus the two
orphaned methods from MinistraModule (getName/getVersion stay — they come
from BaseModule/ModuleInterface), and deletes the now-empty Core/Boundary/.
Test contract (InterfaceContractTest) loses the three BoundaryInterface
assertions; docs (en/ru architecture + modules, .github instructions) now
describe isolated subsystems like Ministra as a convention — own entry
point + bootstrap — rather than a marker interface.
Verified: php -l, make gates, make phpstan (No errors);
InterfaceContractTest + ArchitectureTest green (33 tests, 96 assertions).
DatabaseHandler no longer manually require_once's Database.php — PSR-4
autoloading resolves the base class (verified on-server: both classes
class_exists() without the require). Also removes the unused
ServiceContainer import (docblock-only reference).
Strips 12 dead `use XcVm\Core\Database\Database;` imports that only
appeared in comments/docblocks; the 3 files that reference the base
type (bootstrap, ScannerCommand, admin/api view) keep theirs.
No behavioural change: Database and DatabaseHandler stay split; the
hot query() path is untouched. Full class merge intentionally deferred.
Verified: php -l, make gates (check-procedural-use), make phpstan
(No errors); console status green on 45.90.13.217 (bundled php/xcvm_core).
The header's Usage block only listed --duration/--json/--ua; expand it to the
complete run command, every argparse option (--stall-timeout, --tolerance,
--live, --prebuffer, --buffer-target, --no-color) with defaults, and a couple
more examples. Matches `--help` 1:1. No code change.
Relocate the dev-tooling config into build/ so the project root only holds
source and first-class project files:
- phpstan.dist.neon -> build/phpstan.dist.neon
- phpstan-baseline.neon -> build/phpstan-baseline.neon
- .php-cs-fixer.dist.php -> build/.php-cs-fixer.dist.php
- .php-cs-fixer.cache -> build/ (regenerated there; gitignored)
Because neon/CS-Fixer resolve relative paths against the config file's own
directory, the internal references are re-anchored one level up (src/ ->
../src/, tools/ -> ../tools/, Finder in(__DIR__.'/../src'); the baseline's
path: entries likewise). The Makefile now points PHPStan at the config with
-c build/phpstan.dist.neon (it previously relied on root auto-discovery),
generates the baseline into build/, and passes --config=build/... to CS-Fixer;
the CS-Fixer cache is pinned to build/ via setCacheFile and re-gitignored.
No behaviour change. Verified: make phpstan (No errors), make cs (0 fixable),
make gates. CI runs through these make targets, so it is covered.
--force only overrode the Tuesday-only gate; the per-database download decision
still skipped whatever was already up to date, so `cron:maxmind --force` just
printed "[SKIP] already up to date" and changed nothing.
--force now also bypasses the freshness check in both code paths:
- GitHub GeoLite2 fallback: re-download regardless of the md5 match.
- MaxMind API: drop the If-Modified-Since header so the server returns the
database (200) instead of 304 Not Modified.
Verified on a live box: `cron:maxmind --force` now reports "[OK] updated" for
all three GeoLite2 databases and their mtimes advance (previously [SKIP]).
- Remove an orphaned docblock (it described createChannelItem but sat above
buildSubtitleImport, left behind by an earlier extraction).
- Refresh buildLive()'s docblock: it is now the sole command builder, not a
shadow — the "run in shadow before it replaces the inline assembly" note was
stale after the flip (0bd6a9dc).
- deleteCache(): if(!empty){…}else{return} -> early-return guard clause.
- updateStream()/updateStreams(): extract the identical check-then-insert of the
cache-invalidation signal into insertCacheSignalOnce(); also drops the
redundant double return in updateStreams and the unconditional getMainID()
when caching is off.
Documentation/cosmetic + a behaviour-preserving dedupe. Refs: #148
Verified: php -l, make phpstan (No errors), make gates. Serve check on the hot
updateStream path (warm 567 -> m3u8 200/#EXTM3U).
Completes the in-progress FFmpeg-command migration. startStream's ~180-line
inline command assembly (and the shadow buildLive() diff-check that ran beside
it) are gone; the command is now built solely by the pure buildLive(), fed the
raw stream row. The delay-playlist bookkeeping (segment start + sleep window)
is hoisted just above the call since buildLive() consumes segmentStart /
delayActive. Net -171 lines.
buildLive() already did all transcode-attribute resolution and {TEMPLATE}
substitution internally, so feeding it the raw row (instead of the
post-mutation row the shadow used) removes a latent double-mutation in the
custom-transcode branch (transcode_profile_id == -1): json_decode((string)
$array) on an already-decoded value would have yielded [] there. Only the
inline path ever ran, so this was never a live bug — but it made the shadow an
invalid equivalence probe for that branch, which is why the flip is verified
directly rather than via the shadow log.
Equivalence verified two ways:
- Structural: the 129 command-building statements of buildLive() are
line-for-line identical to the inline block (the sole non-command delta was a
dead `$rFFPROBE = FFPROBE_BIN_40` assignment that never entered the string).
- Empirical: on a live box, the generated _.ffmpeg is byte-identical
(md5-equal) before vs after the flip for BOTH a plain stream and a
custom-transcode stream (enable_transcode=1, profile_id=-1, custom attrs),
and the flipped stream serves (m3u8 200/#EXTM3U, ts 200 ~2.8MB).
Refs: #148
Verified: php -l, make phpstan (No errors), make gates.
- pidFromFileOrColumn() replaces stopStream's two identical "read the PID from
the sidecar file, else fall back to the streams_servers column" blocks (the
monitor_pid and pid lookups). stopStream no longer needs its own $db handle.
- resetStreamServerRow() replaces the 11/12-column NULL-reset UPDATE duplicated
in stopStream (with monitor_pid) and stopMovie (without) — the difference is
now a $rWithMonitor flag.
Pure mechanical extraction, behaviour preserved. Refs: #148
Verified: php -l, make phpstan (No errors), make gates. Canaried live: warmed
stream 567 (pid+monitor running, sidecars present) then StreamProcess::stopStream
(567, true) -> row reset to pid/monitor NULL + status 0 and all _* sidecars
removed.
Two byte-identical blocks were copied across the stream launchers:
- writeStreamKeyIv() — the AES-128-CBC _.key/_.iv sidecar generation,
repeated verbatim in startStream, startLoopback and
startLLOD (3x)
- clearStreamPidSegments() — the "rm _*.ts + unlink stale _.pid" preamble,
repeated in startLoopback and startLLOD (2x)
Pure mechanical extraction, no behaviour change. Refs: #148
Verified: php -l, make phpstan (No errors), make gates. Canaried live via a
warmed stream (567): startStream produces a 16-byte _.key and 16-byte _.iv and
the m3u8 serves (HTTP 200, #EXTM3U).
The two arms building $rRows ran byte-identical queries — the active
on-demand stream list and the attached-restreamer counts — differing only in
where viewer counts came from (Redis getStreamConnections vs a lines_live
COUNT). Collapsed to one path: fetch the stream IDs and attached counts via the
new ConnectionTracker helpers once, then branch solely on the viewer-count
source. ~58 lines to ~27, and the DB arm's non-$r-prefixed locals
($online/$attached/$stream_id) are gone.
The empty-list usleep+continue guard, previously only on the DB arm, now covers
both (same timing, just skips the redundant work under Redis too). Behaviour
otherwise identical: same idle test (0 viewers, 0 attached, age>=30) and kill
path. Refs: #148
Verified: php -l, make phpstan (No errors), make gates. Not live-canaried on
purpose — this daemon kills streams; validation is static + logic-preservation.
Three DatabaseAware helpers the OndemandCommand daemon can share instead of
inlining the same SQL twice (once per redis/DB branch):
- activeOnDemandStreamIDs() — streams this server serves on-demand (pid set)
- attachedRestreamCounts() — child restreamers per stream (parent_id, live
feed + monitor); a stream with any must not die
- onlineClientCounts() — DB fallback for live viewers per stream when the
Redis store is off (Redis path uses
getStreamConnections)
Pure extraction, no behaviour change; wired up in the next commit. Refs: #148
Commit a6b6bc92 ("deps: bump the composer group") bumped composer.json /
composer.lock but never re-committed a matching production vendor, so the
shipped src/vendor drifted from the lock: every `composer install --no-dev`
re-materialised the newer packages and left an un-cleanable diff (this is why
`make dev-clean` could not restore a pristine tree).
Regenerated with `composer install --no-dev` and committed the result:
- geoip2/geoip2 v3.3.0 -> v3.4.0 (adds Record/AnonymizerFeed, drops upstream
CLAUDE.md, ships the package's docs/ + mise/lychee tooling as extracted —
matching how other vendored packages are committed, no trimming policy)
- maxmind-db/reader 1.5.12 -> 1.5.13 (installed.php/json metadata; shipped PHP
sources are byte-identical between the two patch releases)
- composer/ca-bundle cacert.pem refresh
The root-package self-reference in installed.php/json is kept at the committed
placeholder (per the dev-clean convention) to avoid per-commit HEAD-sha churn.
Verified: make gates (procedural-use, LB-archive, vendor-prod-only all OK),
php -l on the new/changed geoip2 sources. Refs: #148
CI runs PHPStan from composer.lock (2.2.8), but the baseline had last been
frozen with a stale local dev-tools install (2.1.17). 2.2.8 reports several
identifiers with different wording (e.g. variable.undefined now emits
"Undefined variable: $x" instead of "Variable $x might not be defined"), so
ten baseline entries no longer matched and those pre-existing errors surfaced
in CI while staying hidden locally. reportUnmatchedIgnoredErrors:false hid the
reverse (stale entries silently dropped).
Regenerated with 2.2.8 after syncing dev-tools to the lock. Net effect is a
large shrink (479 lines removed, 47 added): 2.2.8 resolves much of the noise
2.1.17 false-flagged. No source change — these are all pre-existing entries,
now frozen under the tool version CI actually uses. Refs: #148
- OffAirHandler::showNotOnAir() wraps the show_not_on_air_video / path call that
live.php repeated 7 times with an identical 9-arg list.
- NetworkUtils::ipMatches($subnet, $target, $client) replaces the exact / same-/24
ternary that appeared 3 times.
- Initialise $rServerID = SERVER_ID at the top: it was only assigned inside the
`if ($rChannelInfo)` block, so the final off-air call (the channel-less path)
read an undefined $rServerID.
Behaviour preserved. Validated live: ts serves (2.5MB by t=5s); an HLS token
requested twice returns #EXTM3U on both create and update (the update branch runs
the ip-match). Refs: #148
Verified: php -l, phpstan (No errors), make gates.