Commit Graph
187 Commits
Author SHA1 Message Date
Divarion_D cdfaab0b3c docs(licensing): disclose the activation model & data sent to the server
Adds docs/en/info/licensing-and-activation.md (dual-licensing, AGPL §7b
attribution check, what activation gates, how to activate, what the panel
sends to the licence server and what it never sends, provenance), wires it
into the mkdocs nav (+ ru label), and extends the README "License
Enforcement" section with the activation + data-transparency disclosure.

Makes the activation / attribution / phone-home mechanism formally disclosed
(the legal basis for enforcement on top of AGPL). English source only; ru is
generated.
2026-09-19 22:16:48 +03:00
Divarion_D 46c017e9e0 docs: update auth/session guide for the bootstrap consolidation
functions.php and session.php were removed. Repoint the admin session
validation, lifecycle and file-reference sections at
AdminScopeBootstrap::hydrateAdminContext(), the shared
SessionManager::adminSessionValid() integrity guard, and SessionController
(the AJAX session-poll route). docs/ru is regenerated at release.
2026-09-18 11:42:15 +03:00
Divarion_D 13d4d6b077 Prepare release 2.5.2 2026-09-16 22:04:48 +03:00
DanilandGitHub 2b3ae3dc53 Merge pull request #195 from obscuremind/main
tests(e2e)
2026-09-16 19:04:38 +03:00
Divarion_D a453cd9620 docs: fix stale references to the removed prelude files
The prelude shims (Paths/AppConfig/Binaries/ErrorCodes.php) were deleted and the
$rErrorCodes global is gone. Point the developer guides at the new homes:
constants → ConstantsInitializer (paths()/appConfig()/binaries() maps), error
catalogue → ErrorResponder::codes(). Also refresh the now-outdated "refactored
later" note in build/rector.php's skip list.

Only docs/en is edited (docs/ru is regenerated from it before a release);
make docs-build passes.
2026-09-16 18:52:01 +03:00
Divarion_D 323d4aa455 refactor(config): hoist frequently-edited release constants to top-of-file define()s
XC_VM_VERSION / DEV_MODE / DB_ACCESS_ENABLED / DB_ACCESS_PWD are edited on every
release (and by the release automation). Buried as array entries in appConfig()
they were awkward to find and to sed. Move them back to guarded define()s at the
top of ConstantsInitializer.php; appConfig() reads them back, and init() skips
the already-defined ones. The guard keeps a pre-definition (e.g. the PHPStan
stub) from fataling.

Update the release checklist accordingly: the sed commands target the familiar
`define('XC_VM_VERSION', '...')` form in ConstantsInitializer.php again (they were
pointing at the deleted AppConfig.php).
2026-09-16 18:51:51 +03:00
rootandClaude Opus 5 d0458a8eeb test(e2e): drive the admin panel the way an administrator does
The Playwright suite only checked that pages render. It now performs the
administrator's work against a live test panel and asserts the panel's own
data after each step:

- catalogue: a stream category, a bouquet and a reseller package — created,
  renamed / edited, reopened, deleted;
- subscribers: a line with a bouquet (search, edit in the modal, disable /
  enable, ban / unban, delete), a MAG and an Enigma2 device;
- bulk: two lines selected with the header checkbox, disabled and deleted;
- resellers: created with credits, topped up, edited, disabled, deleted;
- block lists: an IP (RFC 5737 address — blocking adds an iptables rule), a
  user agent and an ISP;
- streams: a live stream added with a source and a server, started, running
  with codecs and the Resources column filled in, stopped, renamed, deleted;
- sign-in: a second administrator refused with a wrong password, signing in
  and out — a separate account, because every admin login re-hashes the
  password and ends that account's other sessions.

Records are named `e2e-<run>-…`; a teardown project sweeps whatever a run
leaves behind and nothing else. tools/create-admin.php provisions the
dedicated test administrator on the panel host.

The first runs found three save paths that answered an empty page (fixed in
f7bba5cb, fd13c940, ee2f586a). Against the test panel: 82 passed, 1 skipped
(no series to select).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WDhDajBPziJwjWZcXnh6R
2026-09-16 14:39:39 +00:00
Divarion_DandAbdoAhmedElbanaa 780badaf06 feat(category-templates): admin/reseller UI + custom_data application
Add the Category Templates management UI on top of the already-committed
CategoryTemplateService:
- Admin CategoryTemplate(s)Controller + Ajax controller (create/save/delete/
  clone/toggle-system/apply-all/get); reseller CategoryTemplate(s)Controller
  and views.
- Apply a line's custom_data template layout when building output:
  CategoryService (per-category sync), LineService, MagService,
  PlaylistGenerator, and CacheEngineCronJob (custom_data in the line SELECT).
- Register admin + reseller routes and navbar entries; these shared files
  also carry the Active Codes routes/navbar wiring.

Admin Category Templates views are not part of the re-sent changeset, so the
admin CT pages render empty until those views are added; reseller CT is
complete.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 19:44:01 +03:00
Divarion_D 734f4751c9 chore(tests): move phpunit.phar into tests/ and repoint references
The committed PHPUnit runner lived at tools/.bin/phpunit.phar, away from
the suite it runs. Move it next to the tests it drives —
tests/phpunit.phar — and update every invocation to
`php tests/phpunit.phar -c tests/phpunit.xml.dist`:

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

docs/ru is generated from docs/en (make docs-translate) and is left for
the next regeneration, per the docs workflow.
2026-09-13 22:04:10 +03:00
Divarion_D 98aea670c3 build(rector): add Rector scaffolding (stage 1 — config + make targets + docs)
Adopt Rector as a require-dev tool alongside PHPStan/phpcs for safe, mechanical
refactoring:

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

No source code is changed by this commit — scaffolding only. `make rector-fix`
output will be reviewed separately.
2026-09-13 16:48:17 +03:00
rootandClaude Opus 5 74ef365f7d feat(streaming): tamper-proof stream tokens (AES-256-GCM), switched on per panel
Stream-link tokens were AES-CBC with a fixed IV and no MAC. A modified token
decrypts to modified bytes, and a padding error answers differently from a bad
credential (auth.php: BAD_TOKEN vs everything after), so with enough requests
anyone holding a link could read its username and password, or write a token of
their own. Several consumers trust a token's contents as they stand: the live /
vod / timeshift JSON (user_info, channel_info), HLS segment and key tokens, the
web player's proxy URL (fetched server-side) and the MAG portal's verify token
(passed to igbinary_unserialize).

Encryption::seal()/open() add AES-256-GCM with a random nonce, as
base64url(nonce ‖ ciphertext ‖ tag) — the same URL-safe alphabet, so no nginx
route or pattern changes. Every stream-link token is now made with
mintToken() and read with readToken(); StreamTokenCallSitesTest keeps new code
from calling the legacy encrypt()/decrypt() for one. Deterministic encryption
of stored data (HMAC keys looked up by ciphertext, image cache names) stays as
it was.

The new setting secure_stream_tokens (Settings → Tamper-proof Stream Tokens):
- on: tokens are sealed, and the legacy format is refused wherever a token's
  contents are trusted. /play/ playlist and portal links, RTMP tokens and
  probe's /play/ links still read the old format — they carry credentials that
  are looked up again, and saved playlists hold them — and every token auth.php
  cannot read now counts against the address (BruteforceGuard), which stops
  reading an old one through the error responses.
- off: legacy tokens are minted and every format is read.
Servers on an older version cannot read sealed tokens, so migration 021 turns it
off on a panel that has other servers (on for a single server, and for new
installs); turn it on once every server is updated.

key.php now also refuses a token that does not read, instead of serving the key
of stream 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbYsGKhirq9eRK8e6wsCHR
2026-09-13 08:49:06 +00:00
rootandClaude Opus 5 2e471099c0 fix(auth): the login flood limit blocks addresses again
The admin and reseller login pages block an address after login_flood failed
sign-ins in 24 hours. They counted them with
TIME_TO_SEC(TIMEDIFF(NOW(), `date`)) <= 86400, but login_logs.date is an
int(11) Unix timestamp: TIMEDIFF of a DATETIME and an integer is NULL, so
no row ever counted and no address was ever blocked. Checked on MariaDB
11.4: with two failures in the last day the old query counts 0, the new one
2. And failures were only written when "save login logs" was on, so even a
working count would have seen nothing with logs off.

Both pages now ask Authenticator::loginFloodExceeded($ip, $limit), which
compares `date` with time() - 86400, and failed sign-ins (INVALID_LOGIN) are
always recorded — they are the limit's memory; save_login_logs still governs
every other outcome. The auth guide documents the limit, and catches up with
the session-id renewal and cookie flags from 010cdb1b.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HNXXkamPAhGah3U3SwHn8z
2026-09-13 08:34:00 +00:00
rootandClaude Opus 5 76ac4a488c fix(streams): the Resources column shows CPU from the first pass, and keeps showing it
CPU is a difference between two /proc readings, and the previous one was kept
in the stream's progress_info row — so a figure depended on that value
surviving a round trip through a row other code also rewrites, and the first
pass of every producer showed a dash for a minute.

The previous reading now lives beside the stream's files, in
<streams>/<id>_.usage (tmpfs, removed with the rest of <id>_* when the stream
stops): node-local bookkeeping, like the pid file. Where there is no usable
previous reading — a producer's first pass, or a new pid after a restart — the
lifetime average stands in, as ps reports it, computed from the process's own
start time in /proc/PID/stat against /proc/uptime rather than /proc/PID's
mtime, which is only set when something first looks at the directory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WwKmPG4RPK4cnJRAxQhPdL
2026-09-12 21:27:38 +00:00
Divarion_D dbdab9b050 Prepare release 2.5.1 2026-09-11 20:12:09 +03:00
obscuremindandClaude Opus 5 73b31f47c8 docs(streaming): daemon feeds, kicks, HLS key and byte ranges
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:19:51 +01:00
rootandClaude Opus 5 2347cba1b8 fix(streams): a supervised stream must fill stream_info, not only the columns
The daemon reads a stream's codecs and picture size off the bytes it fans out
and reconcileSupervised copied them into `video_codec`, `audio_codec`,
`resolution` and `bitrate` — but not into the `stream_info` JSON, which is the
shape the rest of the panel actually reads. A supervised stream therefore
showed "? x ?" and "N/A" in the streams list; worse, every adaptive variant was
dropped from the master playlist for want of a width, and stream/auth.php fell
back to calling every stream h264 when handing the viewer its codec.

The JSON is now written beside the columns, merged rather than replaced, so
whatever ffprobe once found that the daemon does not read (frame rate,
container) survives, and an unchanged reading writes nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZP7fc2Hz36wbPmuLd9F9o
2026-09-11 11:03:06 +00:00
rootandClaude Opus 5 bb949d18b7 feat(admin): show each stream's producer, CPU and memory on the streams page
A new "Resources" column between Stream Info and Actions: which process is
producing the channel (the fanout daemon's native remuxer, ffmpeg, or PHP for
the LLOD segmenter / loopback relay), the CPU it is burning and the memory it
holds. With the native remuxer now an option per stream, "what does this
channel actually cost" and "which backend is it on" are the two questions the
list could not answer.

ProcessManager reads both from /proc/PID/stat (fields 14/15 and 24, with the
page size derived rather than assumed — 64K pages are normal on arm64). CPU
there is cumulative, so a percentage needs two readings: cron:streams samples
each producer once a pass and folds cpu/mem/producer into the stream's
progress_info, carrying the previous reading in the same JSON to subtract from.
cpuPercent() returns null rather than a wild figure when the pair says nothing
— no previous sample, same instant, or a counter that went backwards because
the producer restarted. Only the node running a stream can read its own /proc,
so the sampling happens there and reaches the panel in the row the cron already
writes; MAIN just renders it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZP7fc2Hz36wbPmuLd9F9o
2026-09-11 10:58:50 +00:00
rootandClaude Opus 5 02fba11819 fix(streams): the live type key is live, and the schema defaults are not refusals
Two reasons the native remuxer never ran on a real panel, both in the
eligibility check:

- it compared `type_key` against `live_streams`, which is no type at all —
  `streams_types` holds (1, 'Live Streams', 'live'), (3, 'created_live'),
  (4, 'radio_streams'). Every ordinary live channel was refused, so
  `fanout_source_backend` native/auto silently kept running ffmpeg. The new
  log line said it out loud ("ffmpeg runs this stream: not a live channel"),
  which is how it surfaced; the refusal now names the type it saw.
- `gen_timestamps` and `read_native` were treated as "the operator asked for
  timestamp repair / realtime pacing", but both DEFAULT to 1 in `streams`, so
  they carry no intent and refusing them refuses everything. -re paces a
  file-ish input, which a passthrough of a live source does by itself, and
  genpts only synthesises timestamps a source failed to send — a source that
  broken has no usable video clock either, which ends the run with exit 3 and,
  in `auto`, hands it to ffmpeg.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K48c64npichw9ZCZDU16ja
2026-09-11 10:44:23 +00:00
rootandClaude Opus 5 269bee0148 feat(streams): say which producer runs a stream, and never hand remux to an old daemon
Three things an operator could not see, all in the file they already open:

- the command handed to the supervisor is recorded beside the stream's files
  the way the self-launched path records its ffmpeg line — <id>_.fanout for the
  native remuxer, <id>_.ffmpeg for ffmpeg (in auto, both: the second is the
  fallback). A supervised stream used to leave no record at all.
- when the native backend is on and a stream runs ffmpeg anyway, the reason is
  appended to <id>.errors ("[panel] ffmpeg runs this stream: Generate PTS is
  on"). isNativeEligible() becomes nativeRefusal(), returning that sentence
  instead of a bare false, because the answer is always one of these settings.
- the panel only composes `xc_fanout remux` when the node's daemon advertises
  it (features in GET /monitors/state, FanoutClient::supportsRemux). An older
  binary does not reject that command, it misparses it — "remux" reads as a
  positional argument, the process tries to become a second daemon on sockets
  the running one holds, and the stream never starts. On a node whose panel was
  updated first, streams now keep running ffmpeg and say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K48c64npichw9ZCZDU16ja
2026-09-11 10:30:07 +00:00
rootandClaude Opus 5 2c0e765a33 docs: stream supervision and the native remuxer
How live streams are handed to the xc_fanout supervisor, when a copy-only
stream runs `xc_fanout remux` instead of ffmpeg, how streams_servers is
kept in step, and what still runs under the PHP monitor. Also the
remuxer case of ProcessManager::isStreamRunning(), StreamProcess::
isWatched(), the monitor command's stand-down, and the two settings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012QL93N6dzGkmKoQgA4oh16
2026-09-11 09:20:03 +00:00
Divarion_D 356c181652 Prepare release 2.5.0 2026-09-10 17:31:22 +03:00
Divarion_D 4556c4d54c docs: drop dts-audio-test refs, refresh test-generator blurb, reformat ffmpeg ADR
- CONTRIBUTING.md / tools/README.md: remove the deleted dts-audio-test row
  and update the test-stream-generator description (now generates a
  synthetic pattern, no input file).
- docs/adr/plan-ffmpeg-per-distro-distribution.ru.md: table/list whitespace
  reformat only.
2026-09-09 21:34:20 +03:00
DanilandGitHub fde35439e3 Merge pull request #181 from Vateron-Media/feat/new-adminUI
Feat/new admin UI
2026-09-08 20:39:11 +03:00
Divarion_D 7ce620a864 docs(dev): document module provider contracts & controller/REST wiring
- module-extension-points: Topbar, Table, Permission and Quick Tools provider
  sections + the rule that a module owns its table end-to-end (clean JSON,
  module api routes, events — core never touches module tables).
- module-authoring: list the optional providers in the sub-contract tree and
  the method table.
- core-wiring: bootAll steps 6–9 (topbar/table/permission/quick-tools) and that
  registries run without a router.
- http-request-handling: REST path boots modules (registries only) and serves
  module tables generically via TableRegistry.
2026-09-08 20:15:54 +03:00
Divarion_D 175198a902 refactor(stream-check): merge checker and grapher into one stream_check.py
Consolidate the two stream-check tools into a single master script with
three subcommands:

- check <url>          verify one stream (or --live dashboard)
- playlist <path|url>  batch an .m3u list -> aggregate JSON (+ per-stream files)
- graph <inputs...>    render the JSON as SVG charts

Shared helpers (HTTP, slugify, m3u/JSON handling) are now defined once.
The old --playlist flag becomes the `playlist` subcommand and the bare-URL
form becomes `check <url>`; the streamtest harness is updated accordingly.

Also fix the per-stream SVG rendering black in viewers that do not support
8-digit #rrggbbaa hex: the bitrate area fill and not-PLAYING bands now use
6-digit hex plus a separate fill-opacity attribute.

Docs (English + tools READMEs + STREAMTEST) updated to the new invocation.
Removes stream_queue_check.py and stream_graph.py.
2026-09-06 11:11:10 +03:00
Divarion_D defeae3a3c Prepare release 2.4.3 2026-09-01 16:07:06 +03:00
Divarion_D afde118270 refactor(rbac): delete dead config/permissions.php, superseded by PermissionReference
src/config/permissions.php was never loaded anywhere and was internally
broken (built its rows through an undefined $language). Its 100-key list and
[key, title, text] row-building are duplicated verbatim by
XcVm\Core\Reference\PermissionReference (keys()/advanced()), the live source
the group editor (Views/admin/group.php) already uses. Its returned
$rPermissions was unrelated to the runtime global $rPermissions, which comes
from the users_groups table via AuthRepository::getPermissions().

Update the RBAC guide to point at PermissionReference (docs/en; the ru tree is
regenerated before release).
2026-08-31 21:44:34 +03:00
Divarion_D 1aede3bf4c refactor(i18n): co-locate language files with the Translator
Move the .ini language files from src/resources/langs/ to
src/Core/Localization/lang/, next to the Translator subsystem that owns
them — which already defaulted its $langsDir to __DIR__ . '/lang/'. This
dissolves the now-vestigial src/resources/ bucket (its data/ tree went
with admin_constants, libs/ was empty).

- bootstrap.php calls Translator::init() with no argument, relying on the
  class's own __DIR__-relative default instead of MAIN_HOME . 'resources/langs/'.
- Makefile LB removal list points at Core/Localization/lang (and drops the
  gone resources/langs, resources/libs); LB still ships no UI translations.
- Drop the stale src/resources entries from phpstan scanDirectories and the
  phpunit coverage excludes.
- Update the English docs (translations guide, build-system table); the ru
  tree is regenerated before release.
2026-08-31 21:28:16 +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 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 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
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 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 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 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
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 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 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 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 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 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 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