Files
XC_VM/docs/adr/search-json-contract.md
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

3.9 KiB

Search JSON contract

Goal: the search endpoint returns structured data, not server-rendered HTML, so the new design (and any future client) renders cards itself. Permission checks, status resolution, category/server lookups stay server-side; only markup moves to the client.

Endpoint: ?action=search ({@see \XcVm\Public\Controllers\Admin\Ajax\SearchAjaxController}). The client renderer is src/Public/assets/admin/js/search.js (renderSearchItem(item)), wired into the Select2 quick-search box in common.js.

Envelope

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

Item (common)

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

Every data.actions[] entry is self-describing so the client needs no per-action logic:

{ "kind": "navigate",    "target": "movie?id=512", "icon": "mdi-pencil", "title": "Edit" }
{ "kind": "api",         "entity": "stream", "sub": "stop", "icon": "mdi-stop", "title": "Stop", "enabled": true }
{ "kind": "fingerprint", "id": 512, "context": "stream", "icon": "mdi-fingerprint", "enabled": true }
{ "kind": "credits",     "id": 5, "icon": "mdi-coin", "title": "Add credits" }

kind → client call: navigate(target) · searchAPI(entity,id,sub) · modalFingerprint(id,context) · addCredits(id). enabled:false renders a disabled button.

data by entity

stream / movie / channel / radio / episode

{
  "layout": "live" | "vod",
  "title": "CNN HD", "title_link": "stream_view?id=512" | null,
  "category": "News (+2)",
  "server": "EU-1 (+1)" | "",
  "image": { "url": "cnn.png", "size": 96 },        // live 96 (stream_icon) / vod 512 (movie_image)
  "badge": { "text": "STREAM", "variant": "success" },
  "connections": 128, "connections_link": "live_connections?stream_id=512",
  "status": { "kind": "uptime",   "text": "01h 02m 03s" }
          | { "kind": "progress", "percent": 42 }
          | { "kind": "status",   "code": 5, "label": "DIRECT", "variant": "purple" },
  "rating": { "stars_full": 3, "half": true, "empty": 1, "year": "2021" } | null,  // movies
  "actions": [ Action,  ]
}

series

{ "title": "Breaking Bad", "category": "Drama", "image": {"url":"cover.jpg","size":512},
  "rating": {"stars_full":4,"half":false,"empty":1,"year":"2008"},
  "badge": {"text":"TV SERIES","variant":"danger"}, "seasons": 5, "episodes": 62,
  "actions": [  ] }

user

{ "username":"reseller1", "group":"Resellers", "owner":"admin", "is_reseller":true, "credits":1000,
  "status":{"label":"Active","variant":"info"}, "users_count":12, "lines_count":340,
  "badge":{"text":"USER","variant":"warning"}, "actions":[  ] }

line / mag / enigma

{ "title": "line123" | "AA:BB:CC:…",   // username for line, mac for device
  "device_type": "mag" | "enigma" | null,
  "status": {"label":"Active"|"Banned"|"Disabled","variant":"info"|"danger"|"warning"},
  "owner": "admin", "expires": "2025-01-01 00:00:00" | null,
  "last_active": {"online":true,"stream_id":512,"stream_name":"CNN","online_for":"01h 00m 00s"}
              | {"online":false,"date":"2024-12-01 10:00:00"|null},
  "connections": 5, "flags": {"restreamer":false,"trial":true},
  "badge": {"variant":"pink"}, "actions":[  ] }

Status codes (streams)

Server resolves $rActualStatus (-1…10) exactly as today; label + variant are derived from the existing $rSearchStatusArray constant, so they stay the single source of truth. Codes 1 (running → uptime) and 6 (created-channel encode → progress) are special-cased into the uptime / progress kinds.