Commit Graph
195 Commits
Author SHA1 Message Date
Claude 5e1cef37ce fix(install): keep SSH credentials off argv and disk; pin the node host key
server:install received the root username and password as positional
arguments, visible in ps and /proc/<pid>/cmdline for the whole install, and
LbInstallFlow/ProxyInstallFlow saved them in a plaintext
bin/install/<id>.json for reinstalls. Any SSH host key was accepted.

- The panel writes a 0600 bin/install/<id>.cred and runs
  `server:install <type> <id> <port> - - … --cred-file=<path>`; the command
  reads and deletes the file before connecting (InstallCredentials). Only
  <id>.cred files in bin/install/ are accepted.
- <id>.json keeps the non-secret parameters only; reinstall_server now needs
  root_password in the request. cron:cleanup (MAIN) scrubs root_password from
  files written by earlier releases.
- The node's SSH host key (SHA-1) must match the optional Expected SSH Host
  Key (hex or `ssh-keygen -l -E sha1` output), else the key stored by the
  first install (new servers.ssh_hostkey_sha1, migration 026), else it is
  trusted on first use and stored.
- New install-form field and strings in all eight languages; cli-tools docs.

Phase 0 stop-gap of the MAIN <-> LB API plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Es8UhyPS8MNjuYTagqfGWi
2026-09-25 09:10:04 +00:00
Claude 488c923972 fix(build): strip stale LB manifest paths and gate stale/routed entries
The LB strip lists still named files by their pre-PSR-4 www/ paths, so
those entries matched nothing and the files kept shipping under Public/:
Public/admin/api.php, Public/admin/proxy_api.php, Public/stream/auth.php
and Public/stream/probe.php reached every LB. auth.php and probe.php are
routed by the LB nginx but fatal there on the stripped Domain/User.
LB_DIRS still listed the removed resources/ and www/ trees, and
LB_DIRS_TO_REMOVE listed the non-existent Domain/Auth. The gate never
noticed, because it only checked the SENSITIVE prefixes.

Makefile:
- Drop the dead entries (resources, www, Domain/Auth, GeoLite2-City.mmdb,
  the nine www/* files).
- Strip the MAIN-only files that no LB route or LB code needs: the four
  endpoints above, PlayerV2, AdminAPIWrapper, ResellerAPIWrapper,
  ActiveCodeApiController, ResellerApiDispatcher, ResellerTableRenderer,
  Cli/migration_logic.php and DbMigrateCommand (on an LB it would run
  MAIN's schema migrations). The viewer-API controllers still ship
  because the LB nginx still routes them; they go later with the routes.
- lb_delete_files_list now always writes the LB deleted_files.txt as the
  LB-scoped git deletions plus every stripped code file (Cli, Core,
  Domain, Infrastructure, Public, Streaming; never bin/config/content).
  An update only deletes listed files, so without this the files newly
  stripped here would stay on LBs that are already installed. The
  retired resources/ and www/ trees stay in the git-deletion scope
  (LB_RETIRED_DIRS), so their old entries are still applied.

lb_configs/nginx.conf: stop routing /stream/auth, /stream/probe and
/probe.php. They now get nginx's 404 instead of a PHP fatal. MAIN nginx
is unchanged.

verify-lb-archive.sh now also fails when:
- an LB_* entry matches no tracked path (STALE);
- a script the LB nginx executes is stripped (MISSING): every
  SCRIPT_FILENAME, and Public/<scope>/<handler>.php for each handler the
  /stream/ and /admin/ gateway locations accept.
SENSITIVE covers the new strips, CacheHandlerCommand and
CacheEngineCronJob. The manifest lookups now use here-strings, because
under pipefail an early `grep -q` exit could SIGPIPE the writer and hide
a leak. LbArchiveGateTest runs the real script with each failure mode
injected through MAKEFLAGS.

Behaviour: the LB archive loses 26 files that no LB route reaches (or
that fatal there today), and the next LB update deletes them. It also
deletes any lingering Domain/User or Domain/Device files. Fresh LBs
never had those, so an older LB that still carries them loses rtmp and
viewer-API paths that only worked because of them, just like a fresh LB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQHeypQpfAXTdCysmi7b6a
2026-09-25 01:14:19 +00:00
Claude 6c391f25d9 fix(servers): harden the OPENSSL_EXTRA repair after review
server:sync-openssl-extra now refuses to send when the main's own
published fingerprint differs from the value it read. Run as root, it
could read a config/openssl_extra that xc_vm cannot read, while the
main's php-fpm runs on the default, and push a value that breaks every
LB. The command and cron:root_signals now share one contract:
OpensslExtra::SIGNAL_ACTION, signal() and applySignal(), with
publish() for cron:servers.

Encryption::readToken now tries the sealed format with both values
before the legacy one. A wrong-value legacy decrypt() could return
garbage (about 1 in 256), so a sealed token minted with the replaced
value was refused. Legacy tokens keep that inherent miss rate.

proxy_api trusts the posted server_id and handed any registered proxy
an LB's signal rows, including the main's OPENSSL_EXTRA. It now skips
set_openssl_extra rows until the source-IP binding lands.

Tests cover sync() with TestDb, the signal contract and the case's own
break. The docs cover the check and the repair.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQHeypQpfAXTdCysmi7b6a
2026-09-25 00:56:05 +00:00
Claude 12257af382 docs(design): add the MAIN ↔ LB API communication design spec
Condensed plan (revision 3 + 28 consistency corrections): LB nodes stop
reaching MAIN's MySQL and Redis and move every exchange to a signed,
encrypted cluster API with licence-derived, rotating node tokens.
This is the source design for the task-level implementation plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQHeypQpfAXTdCysmi7b6a
2026-09-24 22:46:03 +00:00
Divarion_D 5616d29124 docs(design): spec a module-extensible reseller navbar registry
Resellers' sidebar is currently a hardcoded PHP array in
layouts/reseller/header.php with no module extension point, unlike
admin's NavbarRegistry/NavbarItem/CoreNavbarProvider. Design a sibling
ResellerNavbarRegistry + ResellerNavbarProviderInterface (mirroring the
existing PermissionRegistry precedent for reseller-specific module
extensions) so modules can register reseller sidebar entries the same
way they already do for admin.
2026-09-24 17:39:15 +03:00
Divarion_D 4ec94502df docs(licensing): no offline time limit after the first confirmation
A key confirmed by the licence server once keeps working offline with no time
limit; the panel only has to reach the server once within 14 days of the key
being issued. Reword the offline section, the expiry note and the closed-network
FAQ accordingly.

Rewrite the ru page as a mirror of en: it still described the old model (a
community install needs no key and never contacts the server, activation only
for white-label, free keys).
2026-09-23 20:47:05 +03:00
Divarion_D 23a3df3038 docs(licensing): describe the attribution + activation key licensing model
Replace the dual-licensing (attribution OR key) description with the
current model: attribution and a per-installation activation key are
both required, verified daily with a 14-day grace period, and gate
multi-server (LB) operation. Adds the paid-module licensing and an FAQ.
2026-09-23 20:00:31 +03:00
Divarion_D aa207c9a39 Prepare release 2026-09-23 14:22:56 +03:00
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