Coverage for src / lexigram / ai / relay / gateway / admin / pages.py: 93%

130 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-08 23:08 +0800

1"""Management pages for the relay gateway admin contributor. 

2 

3Pages are instantiated by the admin runtime from dotted-path handlers; 

4dependencies are resolved from the DI container. Every page renders an 

5explicit unavailable state when a dependency is missing and never 

6injects unescaped values. 

7""" 

8 

9from __future__ import annotations 

10 

11from dataclasses import dataclass 

12from datetime import timedelta 

13from typing import Any 

14 

15from starlette.responses import HTMLResponse 

16 

17from lexigram.ai.relay.gateway.operations.controls import RelayControlsService 

18from lexigram.ai.relay.gateway.operations.health import RelayHealthService 

19from lexigram.ai.relay.gateway.operations.metrics import RelayMetricsService 

20from lexigram.contracts.ai.relay import ( 

21 RelayGatewayError, 

22 RelayPolicyStoreProtocol, 

23 TimeWindow, 

24) 

25from lexigram.logging import get_logger 

26from lexigram.primitives import clock 

27from lexigram.ui import Card, Divider, Grid, StatCard, el, render_to_string 

28 

29logger = get_logger(__name__) 

30 

31_DEFAULT_WINDOW_MINUTES = 60 

32_PAGE_SIZE = 20 

33 

34 

35@dataclass(frozen=True, slots=True) 

36class _PageContext: 

37 """Request-derived pagination parameters.""" 

38 

39 page: int 

40 page_size: int 

41 minutes: int 

42 

43 

44class RelayGatewayOverviewPage: 

45 """Management page at /admin/relay-gateway/overview.""" 

46 

47 def __init__( 

48 self, 

49 health: RelayHealthService | None = None, 

50 controls: RelayControlsService | None = None, 

51 metrics: RelayMetricsService | None = None, 

52 ) -> None: 

53 self._health = health 

54 self._controls = controls 

55 self._metrics = metrics 

56 

57 async def handle(self, request: Any) -> HTMLResponse: 

58 """Render channel health, converter diagnostics, and stream count. 

59 

60 Args: 

61 request: The starlette request. 

62 

63 Returns: 

64 Fully rendered overview page HTML. 

65 """ 

66 channel_count = "N/A" 

67 healthy_count = "N/A" 

68 active_streams = "N/A" 

69 converter = "N/A" 

70 

71 if self._health is not None: 

72 try: 

73 snapshots = await self._health.channel_health() 

74 channel_count = str(len(snapshots)) 

75 healthy_count = str(sum(1 for s in snapshots if s.status == "healthy")) 

76 except RelayGatewayError as exc: 

77 logger.warning("relay_page.overview.health_failed", error=str(exc)) 

78 try: 

79 diagnostics = await self._health.registry_diagnostics() 

80 converter = diagnostics.converter_id 

81 except RelayGatewayError as exc: 

82 logger.warning("relay_page.overview.diagnostics_failed", error=str(exc)) 

83 

84 if self._controls is not None: 

85 active_streams = str(len(self._controls.active_streams())) 

86 

87 page = [ 

88 el( 

89 "h1", 

90 "Relay Gateway Overview", 

91 class_="text-2xl font-bold text-[var(--foreground)]", 

92 ), 

93 el( 

94 "p", 

95 "Operational status of the gateway channels and converters.", 

96 class_="text-sm text-[var(--muted-foreground)] mt-1 mb-6", 

97 ), 

98 Divider(), 

99 Grid( 

100 StatCard(label="Channels", value=channel_count, icon="server"), 

101 StatCard(label="Healthy", value=healthy_count, icon="heart-pulse"), 

102 StatCard( 

103 label="Active Streams", 

104 value=str(active_streams), 

105 icon="radio", 

106 ), 

107 StatCard(label="Converter", value=converter, icon="cpu"), 

108 cols={"default": 1, "lg": 4}, 

109 gap=4, 

110 ), 

111 _dependency_card("health", self._health is not None), 

112 _dependency_card("controls", self._controls is not None), 

113 await _channel_health_card(self._health), 

114 el("div", class_="p-6"), 

115 ] 

116 html = render_to_string(el("div", *page)) 

117 return HTMLResponse(html) 

118 

119 

120class RelayGatewayRoutesPage: 

121 """Management page at /admin/relay-gateway/routes.""" 

122 

123 def __init__( 

124 self, 

125 metrics: RelayMetricsService | None = None, 

126 ) -> None: 

127 self._metrics = metrics 

128 

129 async def handle(self, request: Any) -> HTMLResponse: 

130 """Render paginated per-route metrics. 

131 

132 Args: 

133 request: The starlette request; accepts ``page``, 

134 ``page_size``, and ``minutes`` query parameters. 

135 

136 Returns: 

137 Paginated routes table HTML. 

138 """ 

139 ctx = _parse_pagination(request) 

140 header = _build_header( 

141 "Relay Routes", 

142 "Per-route conversion counts, losses, and failures.", 

143 ) 

144 if self._metrics is None: 

145 return HTMLResponse( 

146 str(header) 

147 + render_to_string( 

148 el( 

149 "p", 

150 "Route metrics service is not registered.", 

151 class_="text-sm text-[var(--muted-foreground)] p-6", 

152 ) 

153 ) 

154 ) 

155 window = TimeWindow( 

156 start=clock.now() - timedelta(minutes=ctx.minutes), 

157 end=clock.now(), 

158 ) 

159 try: 

160 rows = await self._metrics.route_metrics(window) 

161 except RelayGatewayError as exc: 

162 return HTMLResponse( 

163 str(header) 

164 + render_to_string( 

165 el( 

166 "p", 

167 exc.message, 

168 class_="text-sm text-[var(--muted-foreground)] p-6", 

169 ) 

170 ) 

171 ) 

172 start = (ctx.page - 1) * ctx.page_size 

173 visible = rows[start : start + ctx.page_size] 

174 table_rows = [ 

175 el( 

176 "tr", 

177 el("td", r.source.value, class_="py-1.5 pr-3 font-medium"), 

178 el("td", r.target.value, class_="py-1.5 pr-3"), 

179 el("td", str(r.request_count), class_="py-1.5 pr-3"), 

180 el("td", str(r.unsupported_count), class_="py-1.5 pr-3"), 

181 el("td", str(r.stream_failure_count), class_="py-1.5 pr-3"), 

182 el("td", r.quality.value, class_="py-1.5"), 

183 ) 

184 for r in visible 

185 ] 

186 body_content = ( 

187 render_to_string( 

188 el( 

189 "table", 

190 _thead( 

191 ( 

192 "Route", 

193 "Target", 

194 "Requests", 

195 "Unsupported", 

196 "Stream Failures", 

197 "Quality", 

198 ) 

199 ), 

200 el("tbody", *table_rows, class_="divide-y divide-[var(--border)]"), 

201 class_="w-full", 

202 ) 

203 ) 

204 if table_rows 

205 else render_to_string( 

206 el( 

207 "p", 

208 "No route activity in this window.", 

209 class_="text-sm text-[var(--muted-foreground)] py-4", 

210 ) 

211 ) 

212 ) 

213 card = Card( 

214 title=f"Route Activity (last {ctx.minutes}m)", 

215 content=body_content, 

216 ) 

217 return HTMLResponse(str(header) + render_to_string(card)) 

218 

219 

220class RelayGatewayStreamsPage: 

221 """Management page at /admin/relay-gateway/streams.""" 

222 

223 def __init__( 

224 self, 

225 controls: RelayControlsService | None = None, 

226 ) -> None: 

227 self._controls = controls 

228 

229 async def handle(self, request: Any) -> HTMLResponse: 

230 """Render in-flight streams, oldest first. 

231 

232 Args: 

233 request: The starlette request. 

234 

235 Returns: 

236 Streams table HTML. 

237 """ 

238 header = _build_header( 

239 "Relay Streams", 

240 "In-flight upstream streams, oldest first.", 

241 ) 

242 contents = [header] 

243 if self._controls is None: 

244 contents.append( 

245 el( 

246 "p", 

247 "Controls service is not registered.", 

248 class_="text-sm text-[var(--muted-foreground)] p-6", 

249 ) 

250 ) 

251 return HTMLResponse(render_to_string(el("div", *contents))) 

252 streams = self._controls.active_streams() 

253 if not streams: 

254 contents.append( 

255 el( 

256 "p", 

257 "No streams in flight.", 

258 class_="text-sm text-[var(--muted-foreground)] p-6", 

259 ) 

260 ) 

261 else: 

262 rows = [ 

263 el( 

264 "tr", 

265 el("td", s.stream_id, class_="py-1.5 pr-3 font-mono text-xs"), 

266 el("td", s.channel, class_="py-1.5 pr-3"), 

267 el("td", s.model, class_="py-1.5 pr-3"), 

268 el("td", s.request_id, class_="py-1.5 pr-3"), 

269 el( 

270 "td", 

271 s.started_at.strftime("%Y-%m-%d %H:%M:%S"), 

272 class_="py-1.5", 

273 ), 

274 ) 

275 for s in streams 

276 ] 

277 contents.append( 

278 el( 

279 "table", 

280 _thead(("Stream ID", "Channel", "Model", "Request", "Started")), 

281 el("tbody", *rows, class_="divide-y divide-[var(--border)]"), 

282 class_="w-full", 

283 ) 

284 ) 

285 return HTMLResponse(render_to_string(el("div", *contents, class_="p-6"))) 

286 

287 

288class RelayGatewaySettingsPage: 

289 """Management page at /admin/relay-gateway/settings.""" 

290 

291 def __init__( 

292 self, 

293 policy: RelayPolicyStoreProtocol | None = None, 

294 ) -> None: 

295 self._policy = policy 

296 

297 async def handle(self, request: Any) -> HTMLResponse: 

298 """Render the runtime routing policy. 

299 

300 Args: 

301 request: The starlette request. 

302 

303 Returns: 

304 Policy limits and allowlists HTML. 

305 """ 

306 header = _build_header( 

307 "Relay Settings", 

308 "Runtime routing policy enforced by the gateway.", 

309 ) 

310 if self._policy is None: 

311 return HTMLResponse( 

312 str(header) 

313 + render_to_string( 

314 el( 

315 "p", 

316 "Policy store is not registered.", 

317 class_="text-sm text-[var(--muted-foreground)] p-6", 

318 ) 

319 ) 

320 ) 

321 snapshot = await self._policy.load() 

322 channel_list = el( 

323 "ul", 

324 *[ 

325 _channel_item(name, enabled) 

326 for name, enabled in snapshot.enabled_channels.items() 

327 ], 

328 class_="divide-y divide-[var(--border)]", 

329 ) 

330 details = [ 

331 ("Media Schemes", ", ".join(sorted(snapshot.media_allowed_schemes)) or "-"), 

332 ("Media Hosts", ", ".join(sorted(snapshot.media_allowed_hosts)) or "-"), 

333 ("Max Request Bytes", str(snapshot.max_request_bytes)), 

334 ("Max Stream Seconds", f"{snapshot.max_stream_seconds:.0f}"), 

335 ] 

336 detail_rows = [ 

337 el( 

338 "tr", 

339 el("td", label, class_="py-1.5 pr-3 font-medium"), 

340 el("td", value, class_="py-1.5"), 

341 ) 

342 for label, value in details 

343 ] 

344 card = Card( 

345 title="Routing Policy", 

346 content=render_to_string( 

347 el( 

348 "div", 

349 el( 

350 "h3", 

351 "Channels", 

352 class_="text-sm font-semibold text-[var(--muted-foreground)] py-2", 

353 ), 

354 channel_list, 

355 el( 

356 "h3", 

357 "Limits", 

358 class_="text-sm font-semibold text-[var(--muted-foreground)] pt-4 pb-2", 

359 ), 

360 el( 

361 "dl", 

362 *[ 

363 el( 

364 "div", 

365 el( 

366 "dt", 

367 label, 

368 class_="text-sm font-semibold text-[var(--muted-foreground)] py-1", 

369 ), 

370 el("dd", value, class_="text-sm pb-2"), 

371 ) 

372 for label, value in details 

373 ], 

374 class_="divide-y divide-[var(--border)]", 

375 ), 

376 ) 

377 ), 

378 ) 

379 return HTMLResponse(str(header) + render_to_string(card)) 

380 

381 

382def _channel_item(name: str, enabled: bool) -> Any: 

383 """Render an escaped channel entry.""" 

384 badge = el( 

385 "span", 

386 "enabled" if enabled else "drained", 

387 class_=( 

388 "inline-flex px-2 py-0.5 rounded text-xs font-medium " 

389 + ("bg-green-100 text-green-700" if enabled else "bg-red-100 text-red-700") 

390 ), 

391 ) 

392 return el("li", name, badge, class_="flex items-center gap-2 py-1") 

393 

394 

395def _build_header(title: str, subtitle: str) -> str: 

396 """Build the shared page header HTML.""" 

397 return render_to_string( 

398 el( 

399 "div", 

400 el("h1", title, class_="text-2xl font-bold text-[var(--foreground)]"), 

401 el( 

402 "p", 

403 subtitle, 

404 class_="text-sm text-[var(--muted-foreground)] mt-1 mb-6", 

405 ), 

406 ) 

407 ) 

408 

409 

410def _dependency_card(name: str, available: bool) -> Any: 

411 """Render an explicit dependency availability note.""" 

412 color = "bg-green-100 text-green-700" if available else "bg-red-100 text-red-700" 

413 return el( 

414 "div", 

415 el( 

416 "span", 

417 f"{name} service", 

418 class_=f"inline-block px-2 py-0.5 rounded text-xs font-medium {color}", 

419 ), 

420 class_="pt-4", 

421 ) 

422 

423 

424def _thead(headers: tuple[str, ...]) -> Any: 

425 """Build a table header element.""" 

426 return el( 

427 "thead", 

428 el( 

429 "tr", 

430 *[ 

431 el( 

432 "th", 

433 h, 

434 class_=( 

435 "text-left text-xs font-semibold " 

436 "text-[var(--muted-foreground)] uppercase tracking-wider pb-1 pr-3" 

437 ), 

438 scope_="col", 

439 ) 

440 for h in headers 

441 ], 

442 ), 

443 ) 

444 

445 

446def _parse_pagination(request: Any) -> _PageContext: 

447 """Parse pagination query parameters with safe defaults.""" 

448 params = getattr(request, "query_params", None) 

449 raw = dict(params) if params is not None else {} 

450 

451 def _int(name: str, default: int, lo: int, hi: int) -> int: 

452 try: 

453 value = int(raw.get(name, default)) 

454 except (TypeError, ValueError): 

455 return default 

456 return max(lo, min(hi, value)) 

457 

458 page = _int("page", 1, 1, 1_000_000) 

459 page_size = _int("page_size", _PAGE_SIZE, 1, 100) 

460 minutes = _int("minutes", _DEFAULT_WINDOW_MINUTES, 1, 24 * 60) 

461 return _PageContext(page=page, page_size=page_size, minutes=minutes) 

462 

463 

464def _status_badge(status: str) -> Any: 

465 """Render an escaped status badge.""" 

466 color = { 

467 "healthy": "bg-green-100 text-green-700", 

468 "degraded": "bg-yellow-100 text-yellow-700", 

469 "unavailable": "bg-gray-100 text-gray-700", 

470 "failed": "bg-red-100 text-red-700", 

471 }.get(status, "bg-gray-100 text-gray-700") 

472 return el( 

473 "span", 

474 status, 

475 class_=f"inline-flex px-2 py-0.5 rounded text-xs font-medium {color}", 

476 ) 

477 

478 

479async def _channel_health_card(health: RelayHealthService | None) -> Any: 

480 """Render the channel health table, or an explicit unavailable note. 

481 

482 Args: 

483 health: The health service, or None if unavailable. 

484 

485 Returns: 

486 The table element, or an unavailable note. 

487 """ 

488 if health is None: 

489 return _dependency_card("health", False) 

490 rows: list[Any] = [] 

491 for row in await health.channel_health(): 

492 detail = row.detail_code or "-" 

493 rows.append( 

494 el( 

495 "tr", 

496 el("td", row.channel, class_="py-1.5 pr-3 font-medium"), 

497 el("td", row.target.value, class_="py-1.5 pr-3"), 

498 el( 

499 "td", 

500 _status_badge(row.status), 

501 class_="py-1.5 pr-3", 

502 ), 

503 el("td", str(row.model_count), class_="py-1.5 pr-3"), 

504 el( 

505 "td", 

506 ( 

507 f"{row.latency_ms_p50:.1f} ms" 

508 if row.latency_ms_p50 is not None 

509 else "-" 

510 ), 

511 class_="py-1.5 pr-3", 

512 ), 

513 el("td", detail, class_="py-1.5"), 

514 ) 

515 ) 

516 if not rows: 

517 return el( 

518 "p", 

519 "No channel health snapshots available.", 

520 class_="text-sm text-[var(--muted-foreground)] py-4", 

521 ) 

522 return el( 

523 "div", 

524 el( 

525 "h2", 

526 "Channel Health", 

527 class_="text-lg font-semibold text-[var(--foreground)] pt-6 pb-2", 

528 ), 

529 el( 

530 "table", 

531 _thead(("Channel", "Target", "Status", "Models", "Latency", "Detail")), 

532 el("tbody", *rows, class_="divide-y divide-[var(--border)]"), 

533 class_="w-full", 

534 ), 

535 )