Files
XC_VM/docs/en/development/admin-ajax-api.md
T
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

6.9 KiB

Admin AJAX API (?action=)

The admin panel's non-page JSON endpoints are reached as ./api?action=<name> (the front controller's api page). Every action is handled by a dedicated PSR-4 controller under XcVm\Public\Controllers\Admin\Ajax, registered as an API route in src/Public/routes/admin.php and dispatched by Router::dispatchApi().

These endpoints replaced the legacy src/Public/Views/admin/api.php — a single ~4985-line flat chain of if (action == 'x') { … exit(); } blocks. It was extracted action-by-action into the controllers below and retired; only an unknown or removed action still reaches the thin AjaxController fallback.


Dispatch order

src/Public/index.php runs API dispatch before page dispatch for the api page, because the legacy AjaxController page handler exits internally:

./api?action=search
  -> Router::dispatchApi('search')      # registered Admin\Ajax controller — wins
       (falls through only if no api route matches)
  -> Router::dispatch('api')            # AjaxController fallback -> {"result":false}

A registered action never reaches the fallback; an unregistered one does, and the fallback answers {"result":false} (guarded to AJAX-only, like the actions themselves). Admin authentication is already enforced by AdminScopeBootstrap::boot() before any of this runs.

Registration looks like:

// src/Public/routes/admin.php
$router->api('search', [SearchAjaxController::class, 'search']);
$router->api('regenerate_cache', [CacheAjaxController::class, 'regenerate']);

BaseAjaxController

File: src/Public/Controllers/Admin/Ajax/BaseAjaxController.php

An abstract base that emits JSON only (no layout/templates), so it does not extend BaseAdminController. It provides the scaffolding every action reuses:

Method Purpose
ok(array $extra = []) Emit {"result":true} (+ extra keys) and end the request
fail(array $extra = []) Emit {"result":false} (+ extra keys) and end the request
gate(string $type, string $key) Authorization::check() gate; on failure emits {"result":false} and stops
gateAny(array $checks) OR-gate: passes if any [type, key] check succeeds, else fails
requireXhr() Reject non-AJAX requests unless debug mode (PHP_ERRORS) is on
json(array $data, int $flags = 0) Raw JSON body with the correct Content-Type, then exit

A typical action collapses the legacy check → … → echo json_encode(); exit; idiom into a few readable lines:

public function regenerate(): never {
    $this->gate('adv', 'manage_streams');
    // … call a domain service …
    $this->ok();
}

Shared line/device state — LineStateTrait

src/Public/Controllers/Admin/Ajax/LineStateTrait.php carries the enable / disable / ban / unban / kill logic shared by the line, MAG and Enigma2 device controllers. It is a trait (not a base class) because those controllers already extend BaseAjaxController; it declares @phpstan-require-extends BaseAjaxController and abstract ok()/fail() stubs so static analysis and the IDE resolve the inherited helpers.


The controllers

Each controller groups a cohesive set of actions (its class docblock lists them):

Controller Area
CacheAjaxController Cache regenerate/enable/disable, Redis clear, handlers
ServerAjaxController Server add/edit/delete and ops
StreamAjaxController / StreamToolsAjaxController Stream start/stop/restart/purge, lists, reviews
PackageAjaxController Packages/bouquets
UserAjaxController Users, lines, resellers
DeviceAjaxController MAG / Enigma2 devices
EpgAjaxController EPG sources and mappings
StatsAjaxController Stats and graphs
BlocklistAjaxController Blocklists / security
BackupAjaxController Backups, logs, reports
ProviderAjaxController Provider (DataTables) endpoints
MultiAjaxController Bulk (multi) actions over selected IDs
SearchAjaxController Global fuzzy search (see below)
MiscAjaxController Remaining small actions

Global search — structured JSON contract

SearchAjaxController::search() (?action=search) is a fuzzy full-text search across lines, MAG/Enigma2 devices, users, streams (live/VOD/created channels/radio/episodes) and series. It returns structured data, not server-rendered HTML: the client renders each result into a card. Permission checks, status resolution and category/server lookups stay server-side; only markup lives in the browser.

Envelope

{ "result": true, "total_count": 12, "items": [ Item, … ] }

An empty search returns a single no_results item for parity with the styled Select2 dropdown.

Item

{
  "id":     "streams#512",         // stable identity (kept for Select2)
  "url":    "stream_view?id=512",  // primary navigation target
  "text":   "CNN HD",              // plain label (Select2 matching)
  "entity": "stream",              // stream|movie|channel|radio|episode|series|user|line|mag|enigma
  "data":   { … }                  // entity-specific payload
}

Every data.actions[] entry is self-describing, so the client needs no per-action logic — it maps kind to an existing global helper:

kind Client call
navigate navigate(target)
api searchAPI(entity, id, sub)
fingerprint modalFingerprint(id, context)
credits addCredits(id)

enabled: false renders a disabled button. Stream status codes (-1…10) are resolved server-side exactly as before; labels/variants derive from the existing $rSearchStatusArray constant, so it stays the single source of truth.

Client renderer

src/Public/assets/admin/js/search.js (renderSearchItem(item)) dispatches by item.entity to per-entity card builders and wires the self-describing actions. It is loaded before common.js, whose Select2 quick-search templateResult calls it (with a loading-state guard) instead of consuming a server html field.

This changes only the render path, not search matching. The DB gather (batched MATCH … AGAINST full-text with score sorting and IN-clause lookups) is unchanged. If live streams are missing from results, rebuild the stale streams FULLTEXT index on the database (ALTER TABLE streams ENGINE=InnoDB;) — a runtime concern, not a code path.


File Purpose
src/Public/Controllers/Admin/Ajax/BaseAjaxController.php JSON scaffolding (ok/fail/gate/requireXhr/json)
src/Public/Controllers/Admin/Ajax/LineStateTrait.php Shared line/device state actions
src/Public/Controllers/Admin/Ajax/*AjaxController.php Per-area action controllers
src/Public/Controllers/Admin/AjaxController.php Fallback for unknown actions ({"result":false})
src/Public/routes/admin.php $router->api(...) registrations
src/Public/assets/admin/js/search.js Client-side search card renderer