false, 'error' => 'unauthorized'], 401); } $page = max(1, (int) ($_GET['page'] ?? 1)); $perPage = (int) ($_GET['per_page'] ?? 50); if (!in_array($perPage, [25, 50, 75, 100, 200], true)) { $perPage = 50; } $sort = (string) ($_GET['sort'] ?? 'opened_at_ms'); $dir = (string) ($_GET['dir'] ?? 'desc'); $tag = trim((string) ($_GET['tag'] ?? '')); try { Database::requireTicketsTable(); $data = TicketRepository::listPage($page, $perPage, $sort, $dir, $tag); json_out(['ok' => true] + $data); } catch (RuntimeException $e) { error_log('tickets api: ' . $e->getMessage()); $out = [ 'ok' => false, 'error' => 'tickets_table_missing', 'hint' => $e->getMessage(), ]; if (cfg('debug')) { try { $pdo = Database::pdo(); $out['db_driver'] = Database::driver(); $out['tickets_table'] = Database::ticketsTableExists($pdo); if (Database::isMysql()) { $out['mysql_database'] = $pdo->query('SELECT DATABASE()')->fetchColumn(); } } catch (Throwable $diag) { $out['diag_error'] = $diag->getMessage(); } } json_out($out, 503); } catch (Throwable $e) { error_log('tickets api: ' . $e->getMessage()); $out = ['ok' => false, 'error' => 'list failed']; if (cfg('debug')) { $out['hint'] = $e->getMessage(); } json_out($out, 500); }