Commit Graph
1258 Commits
Author SHA1 Message Date
Divarion_D 84dfecb33f fix(stream): send RFC-compliant X-XC_VM-* probe/scan headers
The probe endpoint and the source scanner injected the prebuffer/detect
markers with a stray backslash in the header field-name
(`X-\XC_VM-Prebuffer` / `X-\XC_VM-Detect`), a leftover from the namespace
migration. A backslash is not a valid HTTP token character, so strict
origins/CDNs (e.g. Akamai) answer 400 Bad Request and the probe fails
silently (ffprobe runs with `-v quiet`), breaking Load Maps and stream
checks for otherwise-valid sources.

Use the correct `X-XC_VM-Prebuffer` / `X-XC_VM-Detect` names, matching what
StreamProcess sends and what auth.php/live.php read back.
2026-08-23 16:03:21 +03:00
DanilandGitHub af85102a4f Merge pull request #170 from Vateron-Media/dependabot/github_actions/actions/setup-python-7
ci: bump actions/setup-python from 5 to 7
2026-08-23 14:46:27 +03:00
dependabot[bot]andGitHub 5a7d9459bc ci: bump actions/setup-python from 5 to 7
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-23 11:21:24 +00:00
Divarion_D e84f2ba99b ci: drop lfs:true from release checkouts
Bundled binaries are stored in plain Git now, so the release/pre-release builds no longer need (and must not depend on) a Git LFS fetch — which was failing once the repo hit its LFS budget. The Makefile verify_no_lfs_pointers guard still fails the build if an LFS pointer stub ever slips into the archive.
2.4.0
2026-08-23 14:18:31 +03:00
Divarion_D e0674bad78 chore: store bundled binaries in plain Git, drop Git LFS
Convert the 12 bundled binaries at HEAD from Git LFS pointers to regular Git objects (ffmpeg/ffprobe x3, redis-server, yt-dlp, MaxMind GeoIP2-ISP + cidr, guess, login-bg.mp4) and remove the filter=lfs rules from .gitattributes. Plain-git clone bandwidth is not metered, so this stops consuming the repo's Git LFS budget. HEAD-only: pre-existing commits/tags still reference LFS objects (no history rewrite).
2026-08-23 14:17:36 +03:00
Divarion_D 6c2933cd9b chore(release): prepare 2.4.0
Bump XC_VM_VERSION and changelog.json to 2.4.0; regenerate src/migrations/deleted_files.txt from git diff 2.3.9..HEAD.
2026-08-23 13:49:58 +03:00
Divarion_D 48a69def2b build(docs): unbuffered translate output, cache/venv under build/
Run tools/docs/translate.py with python -u and print per-file progress so 'make docs-translate' shows live output instead of going silent. Relocate the translation cache and venv under build/ (build/docs-cache, build/docs-venv).
2026-08-23 13:49:58 +03:00
Divarion_D 07a80da0fa feat(install): frame the port-configuration prompt
Wrap the port-config instruction line in the printc box, matching the framed sections the installer already prints.
2026-08-23 13:49:48 +03:00
Divarion_D 505f876cde fix(test-stream-generator): serve /stream.ts as a live channel, not from t=0
The /stream.ts endpoint spawned a fresh per-client ffmpeg reading the file from the start, so the panel re-opening a channel always rewound to the beginning. Each client's ffmpeg now seeks to the current live position of the loop (offset = (now - start) %% duration); -ss lands on the nearest keyframe for a fast LLOD start, and clients connecting at the same wall-clock get the same offset (in sync).
2026-08-23 13:49:48 +03:00
Divarion_D 169e2cca2a feat(test-install): e2e streaming test through the panel (streamtest)
New streamtest-gen / -gen-stop / -backup / streamtest commands: configure the panel once by hand, snapshot the DB, then restore + run repeatably. The container runner rebuilds caches, launches the test-stream-generator, fetches the panel's output m3u for the test line, and runs the checker (aggregate JSON + per-stream logs copied out). STREAMTEST.md documents the flow; docker-compose mounts tools/ and exposes port 8088; ignore out/, fixtures/, graphs/.
2026-08-23 13:49:37 +03:00
Divarion_D a530a0599b feat(tools): stream-check — m3u playlist, JSON logs, SVG graphs
stream_queue_check.py: batch --playlist mode, per-stream JSON via --out-dir, TTY-aware summary vs JSON, HLS health judged by rebuffers (TS by stall), 120s default duration, --tolerance/--stall-timeout. New stream_graph.py renders the checker JSON as dependency-free SVG charts (per-stream + --combined comparison, unique colour per stream). Both moved under tools/stream-check/ with a README; tools/README.md and docs/en updated.
2026-08-23 13:49:27 +03:00
Divarion_D 9b5b5cb773 Update docs 2026-08-21 21:56:31 +03:00
DanilandGitHub cba5e76c7b Merge pull request #162 from Vateron-Media/feature/ytdlp-autoupdate
feat(bin): auto-update yt-dlp daily (reuse the binaries self-heal cron)
2026-08-21 18:50:30 +03:00
Divarion_D 4ea2e165b9 feat(bin): auto-update yt-dlp daily via cron:root_signals
yt-dlp resolves media URLs (StreamUtils) but is a static bundled binary that
only refreshes on a panel update — the shipped 2025.07.21 is ~13 months stale,
which breaks extraction. Add self-updating, reusing the existing binaries
self-heal mechanism (no new cron/crontab row):

- New `console.php ytdlp` (YtDlpCommand), modelled on FanoutBinaryCommand:
  resolves the latest upstream `yt-dlp/yt-dlp` release, and on a version mismatch
  downloads the `yt-dlp` asset, verifies it against upstream `SHA2-256SUMS`,
  run-tests `--version`, then atomically swaps + chowns xc_vm. A broken download
  never replaces a working binary.
- RootSignalsCronJob: poll `console.php ytdlp` daily (stamp `ytdlp_check`, 86400s),
  next to the fanout_binary / xcvm_core self-heal — runs on main + LB, first pass
  immediately.
- Settings → Info: show the live yt-dlp `--version`; drop the (now auto-updated,
  always-stale) yt-dlp version row from the README tech stack.

Validated against upstream: asset `yt-dlp`, `SHA2-256SUMS`, checksum match,
`--version` run-test (latest 2026.08.19).
2026-08-21 18:43:36 +03:00
DanilandGitHub 156060e67a Merge pull request #161 from Vateron-Media/chore/phpcs-replace-cs-fixer
chore(cs): replace PHP-CS-Fixer with phpcs + Slevomat (precise unused-import detection)
2026-08-21 18:07:30 +03:00
Divarion_D 8661460868 style: remove unused imports + normalize use/namespace spacing (phpcs)
Apply the new phpcs + Slevomat ruleset across src/ (phpcbf): 85 files. The bulk
are unused `use` imports that PHP-CS-Fixer's no_unused_imports missed (class
name only present in a PHPDoc description), plus blank-line normalization around
the use/namespace blocks. Verified safe: `make phpstan` stays green (0 errors) —
no import used in code or a real docblock type was removed.
2026-08-21 17:55:25 +03:00
Divarion_D 9dd2ac2a48 chore(cs): replace PHP-CS-Fixer with phpcs + Slevomat Coding Standard
PHP-CS-Fixer's `no_unused_imports` is conservative — it treats a class name that
merely appears in a PHPDoc *description* as "used", so genuinely-dead imports
(e.g. `use ...Request;` next to a "Request IP" doc description) were never
flagged. Slevomat's UnusedUses is precise: it parses annotation *types*
(@param/@return/@var), so it keeps docblock-typed imports but removes truly
unused ones — matching what Intelephense (P1003) reports.

- Swap require-dev: friendsofphp/php-cs-fixer -> squizlabs/php_codesniffer +
  slevomat/coding-standard (+ phpcodesniffer-composer-installer, allow-listed).
- New narrow ruleset build/phpcs.xml.dist (import/namespace hygiene only, NOT
  full PSR-12): UnusedUses (searchAnnotations=true), UseFromSameNamespace,
  UseDoesNotStartWithBackslash, AlphabeticallySortedUses, UseSpacing,
  NamespaceSpacing. View templates stay excluded.
- Makefile: `make cs` -> phpcs, `make cs-fix` -> phpcbf (same target names).
- CI code-style job, CLAUDE.md, CONTRIBUTING.md, docs, .gitignore updated;
  build/.php-cs-fixer.dist.php removed. Committed vendor stays production-only.
2026-08-21 17:55:24 +03:00
Divarion_D 5b06aa4c46 docs: output MkDocs build to build/site (keep repo root clean)
Move the generated site out of the repo root: site_dir -> build/site, with the
Pages upload path and .gitignore updated to match. build/ already holds tooling
config, so the build output no longer clutters the top-level listing.
2026-08-21 17:17:18 +03:00
Divarion_D db7d55d53d docs(readme): update doc links to the MkDocs URL scheme
The Docsify hash routes (`/XC_VM/#/en-us/…`, `#/ru-ru/…`) no longer exist after
the MkDocs Material migration. Point the README at the new URLs: English at the
site root, Russian under /ru/, and pages as directory URLs.
2026-08-21 17:04:51 +03:00
Divarion_D 683f301a00 Merge branch 'main' of https://github.com/Vateron-Media/XC_VM 2026-08-21 17:01:19 +03:00
Divarion_D b455cf39aa docs: project rule — edit only docs/en, docs/ru is generated
Add a "Documentation" section to CLAUDE.md so every contributor/agent knows the
MkDocs workflow this branch introduces: English (docs/en) is the single source;
docs/ru is generated by tools/docs/translate.py and regenerated locally before a
release — never hand-edit it.
2026-08-21 17:01:03 +03:00
DanilandGitHub ac92e68671 Merge pull request #159 from Vateron-Media/feature/docs-autotranslate
docs: migrate to MkDocs Material with auto-translated ru (English is the single source)
2026-08-21 17:00:50 +03:00
Divarion_D b23adc2593 docs: translate ru navigation labels (nav_translations)
Sidebar/tab labels live in mkdocs.yml nav, not in the translated Markdown, so
the ru site was showing English menu titles over Russian content. Add a
nav_translations map for the ru locale (mkdocs-static-i18n) covering every tab,
section and page title — "User Guide" → "Руководство пользователя", etc. English
nav is unchanged; strict build clean.
2026-08-21 16:52:32 +03:00
Divarion_D dbf535bcc5 docs(translate): robust {N} sentinels + possessive handling; regenerate ru
Fix Markdown-mangling artifacts the free web engine (yandex) produced in the
committed docs/ru, and regenerate the whole tree cleanly (0 fallbacks):

- Sentinel format @@N@@ -> {N}. MT engines are trained to preserve curly
  format-string placeholders, so {N} survives code-heavy lines where @@N@@ (and
  ZZZ…ZZZ, which also duplicated its Z) were split/moved — e.g. the stray
  "@0@@" in the FAQ and "load balancerZ" in the README are gone.
- Possessive: a trailing English `'s` is consumed INTO the masked span and
  dropped on restore. Every sentinel format breaks when a bare `'s` sits right
  after it, and Russian has no possessive `'s`.
- Validate + retry: after restore, any leftover brace fragment triggers a retry
  (the engine is non-deterministic); after a few failures the line stays English
  so a broken token is never emitted.
- Glossary += KeyDB, yt-dlp, Ubuntu, iptables, MAGSCAN.

Regenerated docs/ru (37 files, translators/yandex): no residual sentinels,
mkdocs build --strict clean.
2026-08-21 16:31:01 +03:00
Divarion_D bdbcde97c7 docs: commit generated ru, translate locally before release (not in CI)
CI translation was slow, so move it out of GitHub Actions: docs/ru is now a
committed, generated tree refreshed LOCALLY before a release; CI only builds it.

- pages.yml: drop the setup-python/cache/translate steps — the workflow now just
  installs the build toolchain and runs `mkdocs build --strict` on the committed
  en+ru trees.
- .gitignore: stop ignoring docs/ru (now committed); keep site/ + .docs-cache/.
- Split deps: docs/requirements.txt = build only (mkdocs-material, static-i18n,
  used by CI); tools/docs/requirements.txt = translators (local-only).
- Makefile: docs-venv installs both; docs-build/docs-serve no longer translate
  (translation is the deliberate `make docs-translate` release step).
- updates_checklist.md: new "Regenerate translated documentation" step
  (make docs-translate + docs-build, commit docs/ru with the release commit).
- Commit the generated docs/ru (37 files, translators/yandex).

Rule: edit ONLY docs/en; docs/ru is generated — never hand-edit it.
2026-08-20 22:37:44 +03:00
Divarion_D c55238f85b docs(dev): document daemon delivery, Redis idle-resilience, cold-cache safety
Enrich the English source (ru regenerates automatically) with subsystem
behaviour that was missing or stale:

- streaming-subsystem: live client delivery is now daemon-only via xc_fanout
  (X-Accel handoff, fan-out over a unix socket, control-socket off-air/telemetry,
  fanout_sync reconciliation); on-disk HLS kept only for timeshift/thumbnail/
  analyse. Documented the admin "Send Message" drawtext overlay via
  POST /signal/<uuid>. Replaced the stale generateHLS/chase-read delivery step.
- caching-and-redis: how long-lived daemon connections survive a server idle
  `timeout` close (phpredis silent reconnect without AUTH → non-PONG guard +
  re-authenticated reconnect; getCapacity multi() guard), and cold-cache
  fail-closed defaults in LegacyInitializer::initStreaming().
2026-08-20 22:24:54 +03:00
Divarion_D dcd1d8c860 docs: migrate to MkDocs Material with auto-translated ru from English
Replace the hand-maintained Docsify site (parallel docs/en + docs/ru trees
that had already drifted) with a MkDocs Material build where English is the
single source of truth and Russian is generated at build time.

Engine & structure
- mkdocs.yml: Material theme, site_url for the /XC_VM/ Pages subpath, and a
  two-tab information architecture — User Guide (administration, API/Swagger,
  UI translations, diagnostics, info/FAQ) vs Developer Guide (architecture,
  workflow, security, integrations, build). Files are NOT moved — the split is
  nav-only, so URLs and cross-links stay stable.
- mkdocs-static-i18n (folder mode): English at root, Russian under /ru/, with a
  language switcher. `mkdocs build --strict` validates every link/anchor.

Translation pipeline (tools/docs/translate.py)
- Engine-agnostic via DOCS_TRANSLATE_PROVIDER: translators (free, no API key —
  default), anthropic, deepl, or noop. Per-file sha256 cache so only changed
  English files are re-translated. Markdown-safe: code, URLs, HTML tags and
  glossary terms (XC_VM, FFmpeg, HLS, ...) are masked and never translated.
  A file whose translation fails falls back to English so the build never breaks.
- docs/ru is generated and gitignored — never committed. It is produced locally
  (`make docs-serve` / `docs-build`) and in CI.

CI & tooling
- pages.yml: build-then-upload (setup-python -> install -> restore .docs-cache
  with restore-keys -> translate -> mkdocs build --strict -> deploy), replacing
  the verbatim docs/ upload.
- Makefile: docs-venv / docs-translate / docs-build / docs-serve.
- docs/requirements.txt; .gitignore for docs/ru, site/, .docs-cache.

Migration details
- Removed Docsify control files (index.html, _navbar.md, _sidebar.md, .nojekyll)
  and de-Docsify-ed body links in 6 English files (en-us/ aliases -> relative,
  swagger _media paths, stripped ':ignore' link syntax).
- Preserved the two Russian-only planning docs (no English source) by moving
  them into docs/adr/ as *.ru.md (repo-internal, excluded from the site).
2026-08-20 22:09:34 +03:00
Divarion_D b3631c1007 fix(redis): treat a non-PONG health-check as a dead connection
A long-lived daemon (the watchdog) holds one Redis connection through the
RedisManager singleton. When it sits idle past the server's `timeout`, the
server closes the socket; phpredis then transparently reopens it WITHOUT
replaying AUTH, so the next command answers NOAUTH / returns false — which is
how getCapacity() hit "zCard() on bool".

instance()'s debounced health-check only nulled the connection when ping()
*threw*; a non-true ping reply (the silent-reconnect/NOAUTH state) slipped
through and was trusted for another 30s. Treat any non-PONG reply as dead and
fall through to the existing re-authenticated reconnect (ensureConnected →
XC_VM::redis_connect). Verified live on an LB node: after a server-side
CLIENT KILL, reconnect() restores a working, authenticated connection.

This is the client-side fix; the MAIN redis.conf `timeout` is left unchanged.
2026-08-20 21:10:39 +03:00
Divarion_D fcbe7613f5 fix(streaming): survive a cold cache and a dropped LB Redis socket
Two distinct production faults from the panel logs:

Root — cold/absent file cache (MAIN warnings). The streaming bootstrap read
the servers/blocklist/proxy caches with no default, so when a cache file is
not yet built (fresh boot, cleared tmp, before the first cache build) they
came back null. Every downstream consumer that iterates or in_array()'s them
then warns — or fatals on PHP 8 (`in_array($x, null)`, `array_key_exists(...,
null)`). The two logged sites (StreamRedirector foreach, auth.php enable_proxy
offset) were just the ones that happened to hit it.
- LegacyInitializer::initStreaming(): default every cache-read global to an
  array (matching the existing rBouquets guard). A cold cache now fails closed
  (no servers → not-on-air for that request) instead of warning/fatalling.
- Defence-in-depth at the two use sites: StreamRedirector normalises $rServers;
  auth.php null-coalesces $rServers[SERVER_ID]['enable_proxy'].

LB Redis fatal — ConnectionTracker::getCapacity(). A dropped LB→MAIN socket
makes phpredis multi() return false (its 30s-debounced ping misses the drop),
and zCard() on that bool fatals outside the RedisException catch, taking down
the watchdog. Guard the multi() result and throw RedisException so the existing
reconnect+retry handles it; a still-dead Redis degrades to a 0-count cycle.
2026-08-20 21:00:16 +03:00
Divarion_D e1b13ce932 feat(admin): footer/login branding, version badges, AdminHelpers cleanup
- Footer (getFooter): show "Vateron Media" as a link to the project + an
  AGPL-3.0 license link and version, en-dash year range; rendered on
  admin/reseller/player footers.
- Login page: add a short brand line (project + AGPL-3.0 links).
- Settings → Info: two new version badges — the xc_fanout daemon
  (`-version`) and the xcvm_core extension (version marker), N/A when
  unavailable.
- AdminHelpers: flatten the empty-if/else artifacts (validateCIDR,
  overwriteData, sortArrayByArray, convertToCSV) and translate the class
  docblock to English; behaviour unchanged.
2026-08-20 20:48:30 +03:00
DanilandGitHub 94cf47649b Merge pull request #158 from Vateron-Media/refactor/e2-non-proxy-daemon-only
refactor(streaming): E2/E3 — daemon-only client live delivery (TS + HLS)
2026-08-20 19:54:00 +03:00
Divarion_D 7c8b066e94 refactor(streaming): delete orphaned SignalSender; overlay lives in daemon
The PHP SignalSender byte-path overlay class had 0 callers after E3 moved
the admin "send message" feature into xc_fanout (drawtext on the viewer's
HLS segment / TS window via FanoutClient::sendSignal -> POST /signal/<uuid>).
Delete the class and scrub its now-dangling mentions:

- src/Streaming/Delivery/SignalSender.php: removed (git rm)
- FanoutClient.php: comment reworded (legacy PHP byte-path, not the class)
- docs/{en,ru}/development/streaming-subsystem.md: dropped the tree line
- docs/adr/0003: overlay is e2e-proven on the LB; note the drawtext-ffmpeg
  selection gotcha (bundled 8.0/7.1 lack the filter)
2026-08-20 19:48:27 +03:00
Divarion_D c6bc73caf5 fix(overlay): pick a drawtext-capable ffmpeg for the daemon overlay
The send-message overlay needs ffmpeg's drawtext filter, but some bundled builds
(8.0/7.1) lack it despite libfreetype — so the newest-ffmpeg pick silently no-op'd
the overlay (verified on the LB: 8.0/7.1 no drawtext, 4.0 yes). The service now
scans newest→oldest for a drawtext-capable ffmpeg and uses that, falling back to
the newest (overlay off) if none have it. Proven e2e: green 'HELLO XC_VM OVERLAY'
burned onto a signalled viewer's HLS segment; one-shot cleared after.
2026-08-20 19:39:41 +03:00
Divarion_D 07794d33bd docs(adr): E3 done — client HLS daemon-only; send-message overlay moved to the daemon 2026-08-20 19:24:00 +03:00
Divarion_D 5200a66d6a refactor(streaming): E3 — daemon-only client HLS + send-message overlay via the daemon
Complete the client cutover: HLS is now served to clients ONLY by the daemon, and
the admin "send message" text overlay (previously burned in by the PHP byte path)
moves to the daemon so the cutover doesn't regress it.

E3 (client HLS = daemon-only):
- live.php m3u8 arm: drop the legacy generateHLS fallback — a fed stream serves the
  daemon's in-RAM playlist (plain or AES-128), else not-on-air (like the TS arm).
- segment.php: LIVE segments must be daemon tokens ("<id>_d<seq>.ts") → X-Accel to
  the daemon; a non-daemon LIVE segment now 404s instead of serving on-disk tmpfs.
  Removed the dead legacy LIVE branch (encrypt-on-read + /xc_hls/) and its orphaned
  SignalSender/LegacyInitializer/AsyncFileOperations imports; kept the ARCHIVE
  (timeshift) path untouched. Cleaned empty-if/else guard artifacts along the way.
- On-disk HLS stays (Phase F cancelled) only for timeshift/thumb/.analyse/Monitor —
  never served to clients.

Send-message overlay via the daemon (feature parity — see the daemon's 0.9.0):
- FanoutClient::sendSignal → control POST /signal/<uuid>; InternalApiController's
  signal_send pushes there (legacy tmpfs signal file kept as a harmless no-op).
- segment.php passes ?c=<uuid>&vc=<codec> and live.php passes &vc=<codec> so the
  daemon can burn the banner onto that viewer's HLS segment / TS window.
- service launches the daemon with -ffmpeg (newest bundled) + -font free-sans.ttf.

php -l + make gates green.
2026-08-20 19:23:23 +03:00
Divarion_D dbe33713e5 docs(adr): E2 done on refactor/e2-non-proxy-daemon-only (stale branch superseded)
E2 re-applied on the current feature work; live.php 698->467, chase-read +
orphaned SegmentReader removed; daemon-only non-proxy TS. Gating satisfied (daemon
box-proven across all types on LB this session). E3 stays not-done by decision.
2026-08-20 18:40:26 +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 ad8fb0b7d8 refactor(streaming): E2 — remove the non-proxy .ts chase-read; daemon-only TS
Phase E (ADR 0003), re-applied on top of the current feature work (P4/Phase G/
xcvm_core). live.php's non-proxy TS delivery is now daemon-only: if the daemon
serves the stream ($rTSDaemon / isStreamFed) it X-Accels to the daemon; otherwise
it shows not-on-air, exactly like the proxy arm. The ~230-line legacy per-viewer
chase-read (playlist read → prebuffer wait → segment loop → connection monitoring)
and its now-unused SegmentReader/SignalSender/CacheReader imports are deleted;
live.php shrinks 698→467 lines. php -l + make gates green.

Behaviour change: the daemon becomes mandatory for non-proxy TS — during a
daemon-down / re-feed window a viewer gets not-on-air, not the legacy feed. This
removes PHP from the TS byte path entirely. git revert is the rollback.
2026-08-20 18:37:27 +03:00
DanilandGitHub b0fa0e1f14 Merge pull request #157 from Vateron-Media/feature/tmpfs-free-streaming
Live-delivery cutover to xc_fanout daemon (PHP out of the byte path)
2026-08-20 18:08:44 +03:00
DanilandGitHub 502d2eab01 Merge branch 'main' into feature/tmpfs-free-streaming 2026-08-20 18:07:10 +03:00
Divarion_D 9e3bc17659 style(cs): drop double blank line after namespace in ProcessChecker
PHP-CS-Fixer CI flagged a stray extra blank line between the namespace
declaration and the docblock.
2026-08-20 18:04:17 +03:00
Divarion_D 59bdce10f9 docs(adr): LB<->Redis resolved via xcvm_core self-update; LB e2e fully proven
xcvm_core self-update ships config_set_redis to LB nodes -> configureRedisLb points
Redis at MAIN -> RedisManager connects -> viewer served video/mp2t by the daemon on
the LB (LINE_CREATE_FAIL gone, /connections+/rates populated, php-fpm flat). Item 1
(LB non-proxy source-mode e2e) closed; edge-types remain for coverage.
2026-08-20 16:57:34 +03:00
Divarion_D 9aaf6315ee feat(binaries): xcvm_core extension self-update (unblocks LB Redis)
The xcvm_core PHP extension is mirrored into the binaries repo tree — decoupled
from the heavy per-distro runtime bundle — under bin/xcvm_core/ as per-OpenSSL-ABI
archives (openssl1.1 / openssl3) + version.json + SHA256SUMS. Nothing panel-side
kept it current, so nodes drifted onto whatever build their install archive shipped
— and critically the LB's build lacked XC_VM::config_set_redis, so an LB could not
point its Redis at the main server and every redis_handler connection failed with
LINE_CREATE_FAIL.

Add XcvmCoreCommand (console.php xcvm_core), modelled on FanoutBinaryCommand:
- resolves the latest version from version.json (raw, main branch);
- detects this host's OpenSSL ABI (libcrypto.so.3 -> openssl3, else openssl1.1;
  both present -> prefer openssl3 with a load-test fallback to the other);
- downloads the matched archive, verifies SHA-256;
- installs the .so ATOMICALLY with a backup + fresh-php load-test + rollback, so a
  wrong-ABI/broken extension never takes php-fpm down, then USR2-reloads php-fpm;
- version-compared via a sidecar marker next to the .so (idempotent).

Wired into UpdateCommand (post-update) and RootSignalsCronJob (hourly self-heal),
not LB-stripped, so every node converges. Proven on the canary LB: none -> 2.1.0
(openssl3), config_set_redis appears, StatusCommand::configureRedisLb points Redis
at main, RedisManager connects, and a viewer redirected to the LB is served
video/mp2t by the daemon (/connections + /rates populated, php-fpm flat at 1).
2026-08-20 16:56:07 +03:00
Divarion_D 0518356936 docs(adr): LB e2e — daemon delivery proven on LB, blocked by LB<->Redis extension gap
Deployed current code to the canary LB (was pre-daemon): ffmpeg tee -> daemon
has_data, /live + /hls served on the LB, viewer redirect lands on the LB. Blocked
at LINE_CREATE_FAIL: redis_connect reads creds from config.enc only, the sole
setter config_set_redis is absent in all xcvm_core builds, and the LB config.enc
has no Redis creds. socat fixes the host (refused->NOAUTH) but not the password.
Needs a new xcvm_core with config_set_redis (StatusCommand::configureRedisLb is
the committed forward-looking fix).
2026-08-19 20:03:13 +03:00
Divarion_D a73aebf0a3 fix(lb): point LB nodes' Redis at the MAIN server (config_set_redis)
configureRedis (which calls XC_VM::config_set_redis to sync the extension's Redis
target) runs on MAIN only, so an LB never configured its Redis host — the xcvm_core
extension kept its 127.0.0.1 default, XC_VM::redis_connect() got connection-refused
(LBs have no local Redis; bin/redis is stripped from the LB build), and under
redis_handler live.php failed every connection with LINE_CREATE_FAIL.

Add configureRedisLb: on a non-main node, resolve the MAIN server's IP (private
link preferred, else server_ip) and point the extension's Redis at <main>:6379 with
the shared password READ from settings (never rotated — that stays MAIN's job).
Guarded by method_exists(config_set_redis): a node whose xcvm_core predates that
API no-ops safely and needs its binaries/extension updated to pick this up.
2026-08-19 19:41:41 +03:00
Divarion_D 4eac1d8ca0 fix(logging): stamp origin server_id into panel error logs (LB vs MAIN)
panel_logs entries could not be attributed to their origin node: FileLogger wrote
each record WITHOUT server_id, and ErrorsCronJob::parseLog always tagged rows with
the SERVER_ID of whichever node parsed the file — so a log written on an LB but
collected/parsed as MAIN showed up as MAIN (server_id=1), making 'is this error
from the LB or MAIN?' unanswerable.

- FileLogger::log() now stamps server_id into the record at write time (guarded by
  defined('SERVER_ID') for very-early failures), so each log line is self-describing.
- ErrorsCronJob::parseLog() reads that server_id and inserts it, falling back to the
  local SERVER_ID for legacy files written before the field existed.
- server_id is now part of the dedup hash / `unique` key, so the same error from
  LB and MAIN stays two distinct rows instead of INSERT IGNORE collapsing them into
  one and losing the attribution.

The panel_logs table already has the server_id column; nothing schema-side to add.
2026-08-19 17:53:15 +03:00
Divarion_D 665c791f9e docs(adr): loopback restream-from-origin now feeds the daemon (Phase G)
LoopbackCommand mirrors to the daemon ingest socket (like LlodCommand), so an LB
restreaming from MAIN via php_loopback serves viewers through the daemon, not the
PHP byte path. Only a live-node e2e remains (canary LB currently unreachable).
2026-08-18 20:04:06 +03:00
Divarion_D 7583db12d2 feat(streaming): feed the daemon from LoopbackCommand (Phase G restream-from-origin)
Loopback is how an LB restreams a channel from its parent (MAIN) — it reads
MPEG-TS from <parent>/admin/live itself (no ffmpeg) and segments to disk in PHP.
Like LLOD v3, that left it OFF the daemon: an LB restreaming via loopback served
every viewer through the PHP byte path (worker-per-viewer on the LB), defeating
the fan-out on exactly the nodes P6/G targets.

Mirror the SANITIZED whole-packet buffer into the daemon's push-fed ingest socket
(registerIngest before the loop, best-effort non-blocking fwrite, unregister
after) — the same pattern as LlodCommand. We mirror the cleaned buffer, not the
raw read, because admin/live interleaves 0xFF padding that would break the
daemon's packet parsing. Once fed, isStreamFed() routes LB viewers to the daemon
(/live + in-RAM /hls) via the X-Accel locations just restored to lb_configs.
Daemon unreachable ⇒ null connect ⇒ legacy on-disk HLS only, no behaviour change.
2026-08-18 20:02:43 +03:00
Divarion_D 0c34467192 docs(adr): Phase G install productionised — lb_configs nginx.conf fixed, 0.8.0 released
Root cause of the LB 'no fanout locations' defect: the LB build overrides
nginx.conf with the stale lb_configs/nginx.conf (missing all X-Accel locations).
Fixed. libogg already in getPackages; daemon 0.8.0 released. Only a real-source
stream-to-LB e2e remains (external source/assignment).
2026-08-18 19:53:38 +03:00
Divarion_D 4056d9349e fix(lb): restore streaming X-Accel locations in LB nginx.conf (Phase G)
The LB build overrides bin/nginx/conf/nginx.conf with lb_configs/nginx.conf
(Makefile: cp $(CONFIG_DIR)/nginx.conf ...), and that file had drifted stale —
it lacked ALL three streaming X-Accel internal locations that the main nginx.conf
carries. On a freshly-installed LB node this breaks:
  - P1 HLS: segment.php always emits X-Accel-Redirect: /xc_hls/<file> -> 404
  - P2 live fan-out: live.php emits /xc_fanout/<id> -> 404
  - P2/B daemon HLS segments: /xc_fanout_hls/<id>_<seq> -> 404
i.e. daemon delivery (and even plain on-disk HLS) never worked on a stock LB —
matching the canary LB, whose nginx.conf 'predated' these and was patched by hand.

Add the three internal locations verbatim from the main nginx.conf so a stock
'make lb' node serves HLS + daemon fan-out out of the box. (libogg0 is already in
LbInstallFlow::getPackages for every distro; service starts the daemon keepalive +
fanout_sync on any node; fanout_binary/FanoutSync/RootSignals are not LB-stripped.)
2026-08-18 19:52:08 +03:00