Commit Graph
16 Commits
Author SHA1 Message Date
Claude 4f0cf12713 feat(cluster): signed command channel to nodes (Phase 4, COMMANDS flow)
CommandBus queues typed, panel-signed (`cmd`) commands per node in
cluster_commands: FIFO by seq, dedupe_key for desired state, expiry,
pruned by cron:cluster. New ops: `commands` (long-poll after the node's
high-water, held up to 20 s; frozen while quarantined) and `ack` (own
commands only; raises cluster_nodes.cmd_seq).

For nodes with the COMMANDS flow on (toggled on Cluster Nodes),
ClusterRoute sends NodeRpc::request() as a signed node.rpc and waits for
its ack, and NodeRpc::broadcast() / SignalDispatcher::kill() queue
commands; other nodes and every LB keep the legacy transport.

On the node, `console.php cluster:exec` checks a command again against
the panel key the agent pinned and runs node.rpc through the legacy /api
handlers (InternalApiController::runCommand, NodeRpc::ACTIONS only) or
conn.kill_worker. Root actions stay on the signals table for now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DcNdxHieZuUh3EnMUKt7zf
2026-09-25 13:24:24 +00:00
Claude ad6d136fb7 feat(cluster): 1 s liveness loop for telemetry nodes, with the fleet silence guard (Phase 3)
LivenessService::tick() runs every second in MAIN's signals daemon
(cron:cluster is the minute fallback) and judges the active nodes whose
TELEMETRY flow is on by their silence: ok, suspect after 10 s, offline
after cluster_offline_after_sec (30 s). The result, in
tmp/cluster/health.json, is read through Core\Cluster\ClusterHealth:
ServerRepository::getAll() takes server_online from it for those nodes
(instead of the 90 s last_check_ago rule) and getCapacity() doubles a
suspect node's weight. Each transition rewrites the servers cache and is
audited.

When over half of the judged nodes (at least two) fall silent together,
the fleet silence guard holds every node at its last state instead of
marking any offline, audits it and alerts on the Cluster Nodes page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DcNdxHieZuUh3EnMUKt7zf
2026-09-25 12:54:40 +00:00
Claude f162182a03 feat(cluster): authoritative telemetry from the agent (Phase 3, TELEMETRY flow)
For a node with the TELEMETRY flow on, MAIN writes the server's
watchdog_data, last_check_ago, requests_per_second, php_pids (and,
without the Redis handler, connections/users) from the agent's heartbeat
every 5 s, and its servers_stats row every minute.
HeartbeatService::toWatchdogData keeps the legacy getStats() shape,
pinned by ClusterTelemetryTest against getStats()'s source.

On the LB, Core\Cluster\NodeFlows reads the mode and flows the agent
writes to config/cluster/flows.json. With TELEMETRY on, the watchdog
stops writing its row and only refreshes local.json (nginx rps, fanout
status) for the agent; cron:servers skips its stats row and network.py
is stopped. The Cluster Nodes page toggles the flow per node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DcNdxHieZuUh3EnMUKt7zf
2026-09-25 12:54:40 +00:00
Claude 775362f99e feat(cluster): Cluster Nodes admin page and cron:cluster housekeeping
Servers → Cluster Nodes (permission `servers`) is the admin side of the
cluster CLI, via Domain\Cluster\ClusterAdmin: enrolled nodes with state,
liveness, epoch, token expiry, last heartbeat and agent version; code
enrolments waiting for a SAS (approve / reject); issuing an enrolment
code (shown once); and revocation. The deciding admin is recorded.
Strings in all eight languages.

cron:cluster (enabled by migration 037) deletes expired epochs (erasing
their z), the replay cache and used challenges, unused expired codes and
decided requests after a day. It returns at once on load balancers and
while the API is disabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DcNdxHieZuUh3EnMUKt7zf
2026-09-25 12:39:13 +00:00
Claude 625273d83c feat(admin): cluster settings with server-side checks and a Cluster tab (Phase 1)
Core\Cluster\ClusterSettings owns the defaults and bounds of the 19
cluster settings. normalize() clamps numbers, falls back to the default
for an unknown enum, and refuses rather than clamps where clamping would
change what the admin meant:

- a cluster_api_port outside 1024–65535, or one MAIN already uses
  (broadcast, extra and RTMP ports) or that is reserved (31210, 31290,
  3306, 6379);
- a cluster_main_host that is not a DNS name;
- scan roots that are not absolute paths free of '..';
- https_required, unless MAIN's own HTTPS verifies (httpsSelfProbe: a
  non-IP domain and a certificate with more than 7 days left) and no
  active node blocks it (every active node blocks until Phase 3
  telemetry reports HTTPS);
- enabling the LB API without xcvm_core's cluster API
  (ClusterCryptoFactory);
- API mode for new nodes before the cutover phase.

SettingsService::edit() runs these checks. A refusal fails the whole
save, and its translated message is shown on the settings page. It lives
in Core/Cluster, which ships to LBs, so nodes apply the same bounds.

The settings page gets a Cluster tab after Streaming. It shows every
field, and a status line: the extension and its API range, whether HTTPS
is available, and the derived token grace. The strings come in all 8
languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Es8UhyPS8MNjuYTagqfGWi
2026-09-25 10:28:53 +00:00
Claude 79088ff0ef feat(fanout): add a master switch that stops the xc_fanout daemon
settings.fanout_enabled (migration 027, default on) is a new "Fanout
Delivery" toggle beside the other fanout settings. Streaming\Fanout\FanoutMode
reads it and applies it to a node:

- Off: FanoutMode::applyToNode() writes bin/xc_fanout/disabled, then stops
  run.sh (so it cannot respawn the daemon) and then xc_fanout. It runs on
  MAIN when the settings are saved, and on every node from RootSignalsCronJob
  within a minute. The RootSignals keepalive and the hourly fanout_binary
  self-heal skip the daemon while the flag is set; `service boot` and run.sh,
  which cannot read settings, check the flag file.
- Every daemon-facing gate honours the switch. LicenseGate::fanoutUsable()
  and each FanoutClient control-socket call behave as if the daemon were
  down. IngestFeeder::forStream() returns a no-op feeder, and DelayCommand
  stops queueing segments for it. fanout_sync stops writing the daemon
  config and closes the pid-0 connection rows the stopped daemon left.
- The dashboard no longer reports "xc_fanout down" when it was switched off.

The pre-fanout delivery paths for fanout off follow in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Es8UhyPS8MNjuYTagqfGWi
2026-09-25 10:07:46 +00:00
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
d217bb1b48 feat(settings): choose the fanout debug log from a dropdown
The fanout debug field was free text (empty, "all", or a comma list of
category names typed from the tooltip), so a typo silently meant off.
Make it a dropdown of every value the daemon takes: Off, All categories,
or one category. FanoutConfig::DEBUG_CATEGORIES is now the single list
both the dropdown and debugCats() use; a combination saved before stays
selectable as it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4P9p5BXijMoGXd31tN6Zp
2026-09-18 09:57:17 +00:00
rootandClaude Opus 4.8 ba524d57f9 fix(fanout): make the debug toggle actually reachable from settings
The panel mapped fanout_debug -> the daemon's debug_cats, but nothing could
set it: there was no `fanout_debug` column, so QueryHelper::verifyPostTable
(which filters a settings POST against the table's real columns) dropped it,
and there was no field in the settings form to post it with. The toggle was
inert -- FanoutConfig always wrote debug_cats "" (off).

Add the missing column (migration 025 + the install schema), a text field in
the admin settings form beside the other fanout knobs, and the fanout_debug
label in all seven locales. Empty = off (default), "all", or a comma list of
the daemon's categories; FanoutConfig normalises it as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-18 08:03:53 +00:00
rootandClaude Opus 5 b6ac7d4953 fix(i18n): add the manage_category_templates menu label
CoreNavbarProvider labels the category templates menu entry with
manage_category_templates, which no language file defined, so the raw
key showed in the menu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4P9p5BXijMoGXd31tN6Zp
2026-09-17 08:15:16 +00:00
Divarion_DandAbdoAhmedElbanaa 71602c7cc5 i18n: restore ac_* strings and add the used feature keys
The integration had dropped all 208 ac_* (Active Codes) keys from every
language file while adding new feature keys, so the Active Codes UI rendered
raw key names. Restore the language files to their committed state (ac_*
intact) and append only the 57 new keys that are actually referenced in the
panel (59 unused keys from the re-send are skipped). Additive-only; the files
parse under the app's INI_SCANNER_RAW.

Co-Authored-By: AbdoAhmedElbanaa <115952879+AbdoAhmedElbanaa@users.noreply.github.com>
2026-09-15 20:47:45 +03: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 0603e5d2ab feat(fanout): add the fanout_supervise setting
Whether live streams on each server are handed to the xc_fanout daemon's
encoder supervisor instead of getting a per-stream PHP watchdog
(console.php monitor). On by default: the supervisor does everything the
PHP monitor did, from one process per node instead of one per channel,
and the PHP monitor stays the fallback for a daemon that cannot be
reached.

Touches the places every fanout_* setting lives: migration 018 for
upgrades, database.sql for fresh installs (277/277 columns and values
still aligned), the FanoutConfig mapping that writes `supervise` into the
daemon's config.json (applied live by the daemon, no restart), the
settings UI, the checkbox whitelist in SettingsService, all seven language
files, and the unit test. The source-backend tooltip now says what the
backend means for the panel's own streams as well.

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
rootandClaude Opus 5 553aed89ff feat(fanout): expose the daemon's source backend as a panel setting
xc_fanout 0.12.0 can convert a non-mp2t source to MPEG-TS in-process instead of
spawning an ffmpeg child per stream. For the common IPTV case — HLS whose
segments are already MPEG-TS — that child was doing little more than
concatenating bytes the daemon can concatenate itself, at ~27 MB RSS apiece,
plus a process spawn and a probe window on every on-demand join and reconnect.

The daemon reads which path to take from `source_backend` in its config.json;
this adds the panel setting that writes it.

  auto (default) — convert natively where the daemon's reader can, ffmpeg for
                   everything it declines
  ffmpeg         — always spawn ffmpeg; the pre-0.12 behaviour and the
                   kill-switch if a native pull ever misbehaves
  native         — native only, no fallback. Diagnostic: it answers "what is
                   actually eligible on this node", and a declined source there
                   is a dead channel rather than a slightly more expensive one.

auto is the default because the fallback makes it strictly safer than
ffmpeg-always: anything the native reader will not take (fMP4/CMAF HLS, RTMP,
SRT, RTSP, AES-128 encrypted sources, anything it cannot positively identify)
runs exactly the pipeline it ran before.

A `<select>`, not a numeric input, so it is deliberately kept out of the
numeric-coercion list in settings.php that would otherwise mangle the string.
An unrecognised value maps to auto rather than being written through — the
daemon clamps unknown values itself, but a config file we write should not carry
a backend that does not exist, and a typo must never take channels off air.

Touches the six places every fanout_* setting lives: migration 017 for upgrades,
database.sql for fresh installs (column, INSERT list and VALUES — 276/276 still
aligned), the FanoutConfig mapping, the settings UI, all seven language files,
and the unit test. SettingsService needs no change: its whitelist is only for
checkboxes, and verifyPostTable already maps a posted field to its column.

Verified against the real daemon end to end: the panel writing "ffmpeg" boots
xc_fanout with backend=ffmpeg, and changing it to "auto" is picked up live on
the next config poll. Read-modify-write still preserves daemon keys the panel
does not manage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGCQcWGMg1Wn8RdCxYoioN
2026-09-09 19:39:17 +00:00
Divarion_D 1895ce63d0 feat(admin-ui): migrate lines (Lines) to clean-JSON table
Rebuild the panel's largest management table on the Bootstrap 5 stack. handleLines
now emits structured rows (status code, online/trial/restreamer flags, live
connection count, current stream, owner, formatted expiration + last-connection,
notes, ban/enable state, contact) instead of HTML; the view renders every cell
and the per-row action dropdown client-side via datatables-bs5.

Features: reseller (select2-ajax) + status + search filters, bulk-select
toolbar (enable/disable/ban/unban/kill/delete via action=multi&type=line),
per-row edit + fingerprint iframe modals, native Download-Playlist modal
(output_devices) and WhatsApp-renewal modal, kill/ban/unban/enable/disable/
delete via action=line, and CSV export. $rOrder gains the control + checkbox
offset. Add lines to the allowlist + E2E suite (ajax wait widened to 40s for
this heavy handler) and the `all` / `download_playlist` i18n keys.

Refs: #148 #178
2026-09-02 20:10:57 +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