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.
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 ofif (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 thinAjaxControllerfallback.
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 … AGAINSTfull-text with score sorting and IN-clause lookups) is unchanged. If live streams are missing from results, rebuild the stalestreamsFULLTEXT index on the database (ALTER TABLE streams ENGINE=InnoDB;) — a runtime concern, not a code path.
Related files
| 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 |