diff --git a/2.5.1/404.html b/2.5.1/404.html new file mode 100644 index 00000000..014ca6f2 --- /dev/null +++ b/2.5.1/404.html @@ -0,0 +1,2400 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +Failed to fetch due to CORS. This is expected — use the generated cURL command, Postman or Insomnia. It does not mean the API is broken.
+ XC_VM supports automated and manual database backups with local storage and optional Dropbox upload. +Backups are managed through the admin panel, CLI commands, and a cron job.
+Backups contain the complete database structure and data, except the following tables:
+detect_restream_logs, epg_data, lines_activity, lines_live,
+lines_logs, login_logs, mag_claims, mag_logs, mysql_syslog,
+panel_logs, panel_stats, servers_stats, signals,
+streams_errors, streams_logs, streams_stats, syskill_log,
+users_credits_logs, users_logs, watch_logs
+++Note: Restoring a backup clears all log data. These tables are excluded to keep backup sizes manageable.
+
Backups do not include:
+config/)bin/)tmp/)Settings are in the admin panel under Backups:
+| Setting | +Default | +Description | +
|---|---|---|
automatic_backups |
+off |
+frequency: off, hourly, daily, weekly, monthly |
+
backups_to_keep |
+0 |
+local retention count (0 = unlimited) | +
dropbox_remote |
+0 |
+enable Dropbox upload | +
dropbox_keep |
+0 |
+remote retention count (0 = unlimited) | +
dropbox_token |
+'' |
+Dropbox API token | +
Click Create Backup Now in the backups page. This runs the cron job in force mode:
+ +The cron:backups job checks the schedule on each run:
| Schedule | +Interval | +
|---|---|
hourly |
+3600s | +
daily |
+86400s | +
weekly |
+604800s | +
monthly |
+2419200s | +
Only runs on the main server (is_main=1). Uses PID-based locking to prevent overlapping runs.
mysqldump --no-data (structure) + mysqldump --ignore-table (data, excluding log tables).Click Restore on any backup entry. Requires confirmation.
+Process:
+/home/xc_vm/tmp/restore.sql.++Important: Restore drops the entire database and recreates it. All data not in the backup will be lost.
+
For migration scenarios with selective table import:
+ +This restores to a xc_vm_migrate database for selective data migration, rather than overwriting the live database.
backups_to_keep > 0: keeps only the N most recent files. Oldest deleted first.backups_to_keep = 0: keeps all files (unlimited).dropbox_keep > 0: keeps only the N most recent files on Dropbox. Oldest deleted first.dropbox_keep = 0: keeps all remote files (unlimited).Cleanup runs automatically after each backup via BackupsCronJob.
File: src/Core/Storage/DropboxClient.php
When dropbox_remote is enabled:
.uploading marker file is created during upload..uploading is deleted..error file is created with the error message.Admin panel status indicators:
+| Indicator | +Meaning | +
|---|---|
| Green | +successfully uploaded | +
| Yellow | +currently uploading (< 10 minutes old) | +
| Red | +upload failed (hover for error message) | +
| Gray | +not uploaded | +
Methods:
+BackupService::checkRemoteConnection() // validate Dropbox token
+BackupService::uploadRemote($path, $filename, $overwrite = true) // upload backup
+BackupService::downloadRemote($path, $filename) // download backup
+BackupService::deleteRemote($path) // delete remote backup
+BackupService::getRemote() // list remote backups
+Automated backup cron job. Can be forced with argument 1:
sudo -u xc_vm /home/xc_vm/console.php cron:backups
+sudo -u xc_vm /home/xc_vm/console.php cron:backups 1 # force
+Restore a backup to a migration database for selective import:
+ +Reset to a blank database (destroys all data):
+ +Re-authorize load balancers on MySQL:
+ +Action: backup (requires adv:database permission)
| Sub-action | +Description | +
|---|---|
backup |
+trigger immediate backup (background) | +
delete |
+delete local backup + Dropbox copy | +
restore |
+restore database from backup | +
| File | +Purpose | +
|---|---|
src/Core/Backup/BackupService.php |
+backup/restore logic | +
src/Core/Storage/DropboxClient.php |
+Dropbox API client | +
src/Cli/CronJobs/BackupsCronJob.php |
+automated backup cron | +
src/Cli/Commands/ToolsCommand.php |
+CLI migration and database tools | +
src/Public/Views/admin/backups.php |
+admin panel UI | +
src/Public/Views/admin/api.php |
+API endpoint handler | +
src/Public/Controllers/Admin/BackupsController.php |
+admin controller | +
The reseller system enables multi-level affiliate management with credit-based line provisioning. +Resellers create and manage IPTV lines, MAG devices, and Enigma2 devices within their allocated credits and permissions.
+Admin
+ └── assigns credits + group permissions
+ └── Reseller
+ ├── creates IPTV lines (costs credits)
+ ├── creates MAG devices (costs credits)
+ ├── creates Enigma2 devices (costs credits)
+ └── creates sub-resellers (costs credits)
+ └── sub-reseller has own lines + credits
+Core business logic is in src/Domain/User/ResellerAPI.php. Web controllers are under src/Public/Controllers/Reseller/. REST API is in src/Public/Controllers/Api/ResellerRestApiController.php.
Credits are the currency for all reseller operations. Each action has a cost, and the reseller's balance must cover it.
+| Action | +Cost source | +
|---|---|
| Create line (official) | +package.official_credits |
+
| Create line (trial) | +package.trial_credits |
+
| Create MAG device | +same as line | +
| Create Enigma2 device | +same as line | +
| Create sub-reseller | +permissions.create_sub_resellers_price |
+
Resellers can have custom per-package pricing via override_packages JSON on their user record:
$rOverride = json_decode($rUserInfo['override_packages'], true);
+if (isset($rOverride[$rPackage['id']]['official_credits'])) {
+ $rCost = intval($rOverride[$rPackage['id']]['official_credits']);
+}
+Resellers can transfer credits to their direct reports via the adjust_credits API action. Both balances must remain >= 0.
All credit operations are recorded in users_logs:
| Field | +Description | +
|---|---|
owner |
+reseller user ID | +
type |
+line, mag, enigma, user |
+
action |
+new, extend, edit, adjust_credits |
+
cost |
+credits spent | +
credits_after |
+balance after operation | +
package_id |
+package used | +
date |
+timestamp | +
Lines are IPTV user subscriptions. Types:
+| Type | +Flags | +
|---|---|
| Standard IPTV line | +is_mag=0, is_e2=0 |
+
| MAG device | +is_mag=1 |
+
| Enigma2 device | +is_e2=1 |
+
credits >= cost.exp_date, max_connections, bouquets, allowed_outputs.allowed_ips (JSON), allowed_ua, bypass_ua, is_isplock.lines table via REPLACE INTO.mag_devices or enigma2_devices).Each package specifies available bouquets via bouquets JSON array.
+If allow_change_bouquets permission is enabled, the reseller can select a subset of the package bouquets. Otherwise all package bouquets are auto-assigned.
Managed by MagService (src/Domain/Device/MagService.php).
+Lock fields: ver, device_id2, device_id, hw_version, image_version, stb_type, sn.
Managed by EnigmaService (src/Domain/Device/EnigmaService.php).
+Lock fields: token, lversion, cpu, enigma_version, modem_mac, local_ip.
Both device types support lock_device (hardware binding), is_isplock (ISP binding), and forced_country.
Resellers can create sub-resellers (if create_sub_resellers permission is granted):
owner_id field.create_sub_resellers_price credits.member_group_id must be in the parent's subresellers permission array.Ownership queries:
+Authorization::check('user', $rID) // checks reseller hierarchy
+Authorization::check('line', $rID) // checks if reseller's reports own the line
+Methods:
+UserRepository::getResellers($rOwner, $rIncludeSelf)
+UserRepository::getDirectReports()
+AuthRepository::getGroupPermissions() // builds all_reports recursively
+Permissions come from the users_groups table, loaded via AuthRepository::getPermissions().
| Permission | +Type | +Description | +
|---|---|---|
is_reseller |
+bool |
+user is a reseller | +
create_line |
+bool |
+can create IPTV lines | +
create_mag |
+bool |
+can create MAG devices | +
create_enigma |
+bool |
+can create Enigma2 devices | +
create_sub_resellers |
+bool |
+can create sub-resellers | +
create_sub_resellers_price |
+int |
+credit cost per sub-reseller | +
allow_change_bouquets |
+bool |
+can select bouquet subset | +
allow_change_username |
+bool |
+can set custom username | +
allow_change_password |
+bool |
+can set custom password | +
allow_restrictions |
+bool |
+can set IP/UA restrictions | +
can_view_vod |
+bool |
+can view VOD content | +
reseller_client_connection_logs |
+bool |
+can view connection logs | +
minimum_username_length |
+int |
+minimum username length | +
minimum_password_length |
+int |
+minimum password length | +
PageAuthorization::checkResellerPermissions() maps pages to permissions:
| Pages | +Required permission | +
|---|---|
user, users |
+create_sub_resellers |
+
line, lines |
+create_line |
+
mag, mags |
+create_mag |
+
enigma, enigmas |
+create_enigma |
+
epg_view, streams, movies |
+can_view_vod |
+
live_connections, line_activity |
+reseller_client_connection_logs |
+
What resellers cannot do:
+File: src/Public/Controllers/Api/ResellerRestApiController.php
Authentication via API key. Actions:
+| Action | +Description | +
|---|---|
user_info |
+reseller account info | +
packages |
+available packages | +
get_lines / get_mags / get_enigmas |
+list resources | +
create_line / edit_line / delete_line |
+line CRUD | +
enable_line / disable_line |
+toggle line status | +
create_mag / edit_mag / delete_mag |
+MAG CRUD | +
create_enigma / edit_enigma / delete_enigma |
+Enigma CRUD | +
convert_mag / convert_enigma |
+convert device type | +
get_users / get_user |
+list/view sub-resellers | +
create_user / edit_user / delete_user |
+sub-reseller CRUD | +
enable_user / disable_user |
+toggle sub-reseller status | +
adjust_credits |
+transfer credits to sub-reseller | +
activity_logs / live_connections |
+connection data | +
user_logs |
+sub-reseller activity logs | +
The ResellerAPIWrapper class validates the API key, initializes a session via ResellerAPI, and returns filtered JSON responses.
File: src/Infrastructure/Bootstrap/reseller_session.php
ip_logout setting enabled).reseller (user ID), rip, rcode, rverify, rlast_activity.File: src/Infrastructure/Bootstrap/reseller_functions.php
$rUserInfo and $rPermissions.File: src/Public/routes/reseller.php
Key routes:
+GET /dashboard → ResellerDashboardController
+GET /edit_profile → ResellerEditProfileController
+POST /post → ResellerPostController (form handler)
+GET /api, POST /api → ResellerApiController
+GET /table, POST /table → ResellerTableController
+
+GET /lines → ResellerLinesController
+GET /line → ResellerLineController
+GET /mags → ResellerMagsController
+GET /mag → ResellerMagController
+GET /enigmas → ResellerEnigmasController
+GET /enigma → ResellerEnigmaController
+
+GET /users → ResellerUsersController
+GET /user → ResellerUserController
+GET /user_logs → ResellerUserLogsController
+GET /live_connections → ResellerLiveConnectionsController
+GET /line_activity → ResellerLineActivityController
+GET /tickets → ResellerTicketsController
+| File | +Purpose | +
|---|---|
src/Domain/User/ResellerAPI.php |
+core business logic | +
src/Public/Controllers/Api/ResellerRestApiController.php |
+REST API | +
src/Public/Controllers/Reseller/*.php |
+web controllers | +
src/Public/routes/reseller.php |
+URL routing | +
src/Public/Views/reseller/*.php |
+view templates | +
src/Infrastructure/ResellerApiDispatcher.php |
+AJAX action routing | +
src/Infrastructure/ResellerTableRenderer.php |
+DataTables rendering | +
src/Infrastructure/Bootstrap/reseller_session.php |
+session management | +
src/Infrastructure/Bootstrap/reseller_functions.php |
+initialization | +
src/Core/Auth/Authorization.php |
+ownership checks | +
src/Core/Auth/PageAuthorization.php |
+page-level gating | +
server:diagnose)¶The panel marks a proxy/LB node offline purely from a stale heartbeat (enabled + status = 1 + a recent last_check_ago), which never tells you why the node stopped reporting. The server:diagnose command answers that question.
The command is read-only: it only runs ping/curl/fsockopen probes, SELECT queries, and sudo -n iptables -nL. It never restarts or reconfigures anything.
Implemented by src/Cli/Commands/ServerDiagnoseCommand.php. The command ships in both MAIN and LB builds — the local mode is the whole point of having it on the node.
The mode is auto-selected from where the command runs (server_id in config.ini → is_main in the servers table):
Probes the target node from the outside and reads its panel-side state:
+| Check | +What it tells you | +
|---|---|
| Enabled / Status / Heartbeat | +How the panel currently sees the node (status = 4 means a failed install/provision) |
+
| ICMP ping | +Is the host alive at all | +
TCP to http_broadcast_port |
+Is nginx reachable, or is the port dropped | +
HTTP GET /api |
+Does PHP actually answer behind nginx | +
| Clock offset | +time_offset vs the panel (skew > 30 s can flap the node online/offline) |
+
| Signal queue | +Unconsumed rows in signals for this node (backlog > 120 s = the node's callback loop is stuck) |
+
The probe combinations map to causes:
+/api silent — nginx is up, PHP is not: check php-fpm on the node./api answers but the heartbeat is stale — the node's watchdog daemon (the heartbeat writer) is not running, or it cannot write to the panel DB. Run the local mode on the node.Run this on the silent LB/proxy node itself — the causes usually live there. No argument needed; the node identifies itself from config.ini. Checks:
iptables INPUT chain for a DROP of the main's IP, plus the flood-block marker file. This is the classic "node went silent for no reason" cause: the flood protection drops public IPs silently, and the main's callbacks stop landing.systemctl is-active xc_vm and a local TCP check on the node's own broadcast port.watchdog daemon updates last_check_ago every few seconds. When its MySQL connection to the main drops it waits for the database to come back (retrying every 5 s) and resumes the heartbeat immediately. Older builds exited instead — which made all nodes go offline at the same moment on any MySQL restart/blip on the main, until cron:servers revived them.cron:servers present in the xc_vm user's crontab (if missing, regenerate with rm -f /home/xc_vm/tmp/crontab and restart the service);cron:servers instance hung on its cron lock — a hung instance blocks every subsequent run for up to 30 minutes (acquireCronLock stale timeout), which is exactly how one DB blip keeps a node offline for half an hour. The command prints the holding PID and the kill command.time_offset vs the panel.++Note: the iptables check requires passwordless sudo (
+sudo -n). Without it the check reportscannot check (need sudo iptables)instead of failing — run the command asrootfor a full picture.
Each check prints one aligned [OK]/[WARN] line, followed by a numbered Probable cause(s) summary with the exact fix command where one exists (e.g. the iptables -D INPUT ... -j DROP unblock line).
| Exit code | +Meaning | +
|---|---|
0 |
+No obvious cause found (or the target is the MAIN itself — nothing to diagnose) | +
1 |
+Usage error: missing/unknown server_id |
+
2 |
+One or more probable causes were found and printed | +
Example (local mode, self-blocked main):
+Self-diagnosis on node #3 — LB-Frankfurt (type 1)
+----------------------------------------------------------------
+[OK] Enabled yes
+[OK] Status 1 (online)
+[WARN] Heartbeat last check-in 641s ago (limit 180s)
+[OK] DB → main reachable (this query ran)
+[OK] Ping main reply (203.0.113.10)
+[WARN] Main :8080 closed/timeout
+[WARN] Main in iptables DROP present (+flood marker)
+[OK] Service xc_vm active
+[OK] nginx :8080 listening
+[OK] watchdog daemon running
+[OK] cron:servers in xc_vm crontab
+[OK] Clock offset 2s vs panel
+----------------------------------------------------------------
+Probable cause(s):
+ 1. Heartbeat is stale (641s > 180s): the node stopped reporting — the checks below narrow down why.
+ 2. This node has DROPPED the main's IP 203.0.113.10 in its own iptables (flood/block false-positive). Unblock: `sudo iptables -D INPUT -s 203.0.113.10 -j DROP && sudo rm -f /home/xc_vm/tmp/flood/block_203.0.113.10`.
+| Symptom | +Likely cause | +Fix | +
|---|---|---|
| Pings, port dropped | +Node's iptables blocked the main's IP | +sudo iptables -D INPUT -s <main_ip> -j DROP + remove the block_<ip> marker (the command prints the exact line) |
+
| No ping, no port | +Host down / network partition / external firewall | +Check hosting console, routes, provider firewall | +
Port open, /api dead |
+php-fpm down | +Restart the xc_vm service on the node |
+
/api fine, heartbeat stale |
+Watchdog daemon dead (exits on DB connection loss) | +sudo -u xc_vm console.php watchdog on the node; check DB grants (tools mysql on the main) |
+
| All nodes drop at the same moment | +A MySQL restart/blip on the main hit every node's watchdog at once (fatal for pre-fix builds; current builds wait it out) | +Check the main's MySQL error log around the drop time; update the nodes so the watchdog survives outages | +
| Node flaps online/offline | +Clock skew > 30 s (or recurring MySQL blips) | +Sync NTP on the node; check MySQL stability on the main | +
| Status = 4 | +Install/provision errored | +Re-run server:install from the main |
+
| File | +Role | +
|---|---|
src/Cli/Commands/ServerDiagnoseCommand.php |
+The diagnostic command (both modes) | +
src/Cli/Commands/WatchdogCommand.php |
+The watchdog daemon — writes the heartbeat (last_check_ago) |
+
src/Cli/CronJobs/ServersCronJob.php |
+Babysitter cron — relaunches a dead watchdog | +
src/Cli/CronJobs/RootSignalsCronJob.php |
+Applies iptables blocks (the false-positive source) | +
src/Domain/Server/ServerRepository.php |
+servers table access |
+
See also: CLI Tools, Updating a Server.
+ + + + + + + + + + + + + +Step-by-step guide to updating an XC_VM server. For the internals of the update process, see Update Mechanism.
+++💾 Before updating, it is recommended to create a backup.
+
Step 1. Open the Servers section in the top menu of the panel.
+
Step 2. Select Manage Servers from the dropdown menu.
+
Step 3. Locate the target server in the servers table and click the menu button in the Actions column.
+
Step 4. Select Server Tools from the menu.
+
Step 5. In the Server Tools dialog, click Update Server.
+
Clicking the button inserts an update signal into the database. Within a minute, CRON picks it up and the update runs automatically: the panel is stopped, updated, and restarted. You can track progress via the server version in the Manage Servers table.
Downloads and applies the latest update from GitHub. Usually triggered automatically through the web panel.
+If an update introduces a problem, you can roll a server back to an earlier release directly from the panel. Rollback works per-server, so both the MAIN panel and individual Load Balancers can be downgraded independently.
+Step 1. Open the Servers section → Manage Servers.
+Step 2. Locate the target server and open its Actions menu (the same menu used for updating).
+Step 3. Click Rollback Version. A dialog opens listing earlier releases (newest first). Pre-release builds are tagged (beta).
Step 4. Select the version to roll back to and confirm.
+Clicking Rollback inserts a rollback signal (carrying the chosen version) into the database. Within a minute CRON picks it up and the rollback runs automatically, reusing the same stop → replace → restart flow as an update. Track progress via the server version in the Manage Servers table.
++💾 On the MAIN server a database backup is taken automatically before the rollback, saved to
+/home/xc_vm/backups/pre_rollback_<from>_to_<to>_<timestamp>.sql. Load balancers have no database, so this step is skipped.
The versions offered depend on the server's update channel: on the stable channel only stable releases are listed; on beta you also see (beta) pre-releases.
++⚠️ A rollback does not undo database migrations — they are forward-only. The schema is designed to stay backward-compatible, and the automatic backup is the recovery path if an older build cannot read newer data. Use rollback only as a recovery step.
+
Downloads the specified release from GitHub and applies it, with the same integrity checks and — on MAIN — the automatic database backup.
+ + + + + + + + + + + + + +This guide explains how to create a self-signed SSL certificate to enable secure HTTPS connections for the built-in Nginx server in the XC_VM project.
+++Note: A fresh install already generates a unique self-signed certificate +automatically (the installer runs
+openssland writesserver.key/server.crt+intobin/nginx/conf/before Nginx starts), andCertbotCronJoblater replaces +it with a real Let's Encrypt certificate. Follow this guide only to regenerate +or replace the certificate manually.
SSL (Secure Sockets Layer) encrypts the connection between client and server, ensuring data confidentiality and user trust.
+This tutorial shows how to create a self-signed SSL certificate for the embedded Nginx server in the XC_VM project.
All SSL-related files (key, certificate, and config) are stored in:
+ +Navigate to this directory before proceeding:
+ +Generate a 2048-bit RSA private key:
+ +After execution, the file server.key will appear — this is your private key.
+Keep it strictly confidential — it is used to sign the SSL certificate.
Create a configuration file containing certificate parameters:
+cat > server.cnf << EOF
+[req]
+distinguished_name = req_distinguished_name
+x509_extensions = v3_req
+prompt = no
+
+[req_distinguished_name]
+C = RU
+ST = Moscow
+L = Moscow
+O = XC_VM
+OU = XC_VM
+CN = XC_VM
+
+[v3_req]
+keyUsage = keyEncipherment, dataEncipherment
+extendedKeyUsage = serverAuth
+subjectAltName = @alt_names
+
+[alt_names]
+DNS.1 = XC_VM
+EOF
+Parameter explanation:
+| Field | +Value | +Purpose | +
|---|---|---|
C |
+RU | +Country | +
ST |
+Moscow | +State/Province | +
L |
+Moscow | +City/Locality | +
O |
+XC_VM | +Organization | +
OU |
+XC_VM | +Organizational Unit | +
CN |
+XC_VM | +Common Name (primary hostname) | +
DNS.1 |
+XC_VM | +Subject Alternative Name (SAN) | +
++Tip: For real domain names, replace
+DNS.1 = XC_VMwith your actual domain (e.g.,DNS.1 = panel.example.com) to avoid browser warnings.
Generate the certificate using the private key and configuration file:
+ +Explanation:
+-new -x509 — creates a new self-signed certificate-days 3650 — certificate validity period (10 years)-config server.cnf — uses the custom configurationserver.crt file containing the public certificateAfter completing all steps, the following files should be present in /home/xc_vm/bin/nginx/conf:
| File | +Purpose | +
|---|---|
server.key |
+Private key | +
server.crt |
+Self-signed SSL certificate | +
server.cnf |
+Certificate configuration file | +
Your XC_VM Nginx server is now accessible via HTTPS using the newly created self-signed certificate.
+Browsers will display a “not trusted” warning — this is expected behavior for self-signed certificates.
CN or DNS.1), you must regenerate the certificate.The XC_VM update system is implemented as a multi-layered process, from the web interface to system-level scripts. This approach ensures reliability, automation, and data integrity during panel updates.
+++📋 For a step-by-step guide with screenshots, see Updating a Server.
+
The process begins when the administrator clicks the "Update" button in the web interface.
+update is inserted into the signals table in the database.Every minute, the following CRON job runs:
+ +The root_signals cron job checks for new signals.
+When it detects an update signal, it launches:
Core logic resides in the UpdateCommand class:
At this stage the following actions are performed:
+MAIN or LB).sudo /usr/bin/python3 /home/xc_vm/update "/home/xc_vm/tmp/.update.tar.gz" "HASH" > /dev/null 2>&1 &
+++💡 After the Python updater finishes, it calls
+console.php update post-updatewhich triggers database migrations and post-update cleanup.
Control is transferred to the Python script:
+ +It performs privileged system operations:
+bin/ffmpeg_bin, bin/nginx, bin/nginx_rtmp, bin/php, bin/redis, bin/install, bin/maxmind, bin/certbot, content, backups, tmp, config, signals
++ℹ️ The same archive is used for both installation and update. Filtering happens on the server at update time — the exclude list is defined directly in
+src/update.
Final steps are executed in the post-update phase of UpdateCommand:
MAIN) was updated → create update signals for all Load Balancers.[ Web Interface ]
+ │
+ ▼
+[ DB: "update" signal ]
+ │
+ ▼
+[ CRON → console.php cron:root_signals ]
+ │
+ ▼
+[ UpdateCommand (PHP): download + verify hash ]
+ │
+ ▼
+[ update (Python): extract to /tmp → remove excluded → copy over ]
+ │
+ ▼
+[ post-update → UpdateCommand ]
+ │
+ ▼
+[ Finalize, restart daemons, update version in DB ]
+A server can also be rolled back to an earlier release. This mirrors the update flow above but targets a chosen version instead of the latest — the same signal → CRON → PHP → Python pipeline and the same src/update applier are reused. Rollback is per-server, so MAIN and each LB can be downgraded independently.
Initiation. In Servers → Manage Servers, the per-server actions menu has a Rollback Version item. It opens a dialog listing earlier releases (pre-releases tagged (beta)), fetched via the rollback_versions API action (GitHubReleases::getPreviousVersions()). Choosing a version inserts a signal — {"action":"rollback","version":"X.Y.Z"} — for that server.
CRON trigger. cron:root_signals handles the rollback signal by launching:
UpdateCommand, rollback case).X.Y.Z, strictly older than the current version).backups/pre_rollback_<from>_to_<to>_<timestamp>.sql, aborting if it fails. LB nodes have no database and skip this.GitHubReleases::getVersionFile() (MAIN → xc_vm.tar.gz, LB → loadbalancer.tar.gz), download it, and verify the MD5.Hand over to the same Python updater (src/update).
System + completion. Identical to an update: the Python script stops the panel, replaces the tree (preserving binaries/config/data), and post-update sets the version in the database to the rolled-back release and restarts the panel.
The version list is channel-aware: the stable channel offers only stable releases, beta also offers (beta) pre-releases.
++⚠️ A downgrade does not undo database migrations (they are forward-only). The schema is kept backward-compatible, and the automatic MAIN backup is the recovery path. The Python applier copies over the tree (
+cp -a) without deleting files, so files added by a newer release remain until a subsequent update.
Interactive, always-in-sync Swagger UI for every XC_VM API, generated from OpenAPI 3.0 specifications. Use the API tabs at the top of the page to switch between APIs, or open one directly via the links below.
+| API | +Description | +Auth | +Open | +
|---|---|---|---|
| Admin API | +XUI.ONE-compatible administration — lines, users, streams, VOD, series, servers, settings (104 endpoints) | +api_key + access code |
+Open ↗ | +
| System API | +Internal /api.php — stream/VOD control, stats, processes, files, connections (31 actions) |
+password (live_streaming_pass) |
+Open ↗ | +
| Player API | +XtreamCodes player — Live TV, VOD, Series, EPG | +username + password |
+Open ↗ | +
| Playlist API | +/playlist authentication + playlist generation |
+username/password or token |
+Open ↗ | +
++CORS note: direct "Try it out" calls from the browser to your server may be blocked by CORS. This is expected — use the generated cURL command, Postman or Insomnia instead. The error does not mean the API is broken.
+
Every spec can be imported into Postman, Insomnia or any OpenAPI 3.0 tooling:
+ + + + + + + + + + + + + + +0&&i[i.length-1])&&(p[0]===6||p[0]===2)){r=0;continue}if(p[0]===3&&(!i||p[1]>i[0]&&p[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function K(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],s;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(a){s={error:a}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(s)throw s.error}}return i}function B(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o