get_param('range') ?: 30); return $this->sendSuccess((new DashboardStats())->build($range)); } public function logs(WP_REST_Request $request): WP_REST_Response { $page = max(1, (int) ($request->get_param('page') ?: 1)); $perPage = max(1, min(100, (int) ($request->get_param('per_page') ?: 20))); // The route's enum already rejects anything else; this guards a direct call. $action = (string) $request->get_param('action'); $status = (string) $request->get_param('status'); $action = in_array($action, SyncLog::FILTER_ACTIONS, true) ? $action : ''; $status = in_array($status, SyncLog::FILTER_STATUSES, true) ? $status : ''; $search = sanitize_text_field((string) $request->get_param('search')); $dateFrom = self::validDate((string) $request->get_param('date_from')); $dateTo = self::validDate((string) $request->get_param('date_to')); // The count runs one past the ceiling so "more than this" is distinguishable; clamp for // display and for pagination, which must not offer a page the capped feed cannot reach. $rawTotal = SyncLog::countRecent($action, $status, $search, $dateFrom, $dateTo); $capped = SyncLog::isCapped($rawTotal); $total = min($rawTotal, SyncLog::COUNT_CAP); $hasFilter = $action !== '' || $status !== '' || $search !== '' || $dateFrom !== '' || $dateTo !== ''; $totalAll = $hasFilter ? min(SyncLog::countRecent(), SyncLog::COUNT_CAP) : $total; // Summary strip: the same filters, minus status. $counts = SyncLog::statusBreakdown($action, $search, $dateFrom, $dateTo); $totalPages = (int) ceil($total / $perPage); $page = $totalPages > 0 ? min($page, $totalPages) : 1; $offset = ($page - 1) * $perPage; $recent = SyncLog::recent($perPage, $offset, $action, $status, $search, $dateFrom, $dateTo); return $this->sendSuccess([ 'logs' => $this->mapRows($recent), 'counts' => $counts, 'total' => $total, 'total_all' => $totalAll, // Past the ceiling the screen shows "10,000+" rather than an exact figure. 'total_capped' => $capped, 'page' => $page, 'per_page' => $perPage, 'total_pages' => $totalPages, ]); } public static function validDate(string $raw): string { return preg_match('/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/', $raw) ? $raw : ''; } private function mapRows(array $recent): array { $sites = []; foreach (TargetSite::listAll() as $site) { $sites[$site->getId()] = $site->getName(); } $postIds = array_values(array_filter(array_map(static fn($row) => (int) $row['local_post_id'], $recent))); if ($postIds) { _prime_post_caches($postIds, false, false); } $authorIds = array_values(array_unique(array_map(static fn($row) => (int) $row['author_id'], $recent))); if ($authorIds) { cache_users($authorIds); } $authorNames = []; foreach ($authorIds as $authorId) { $author = $authorId > 0 ? get_userdata($authorId) : null; $authorNames[$authorId] = DashboardStats::authorLabel($authorId, $author ? $author->display_name : null); } return array_map(static function ($row) use ($sites, $authorNames) { $siteId = (int) $row['target_site_id']; $postId = (int) $row['local_post_id']; return [ 'id' => (int) $row['id'], 'at' => (string) $row['created_at'], 'post_id' => $postId, 'title' => SyncLog::postTitle($row), 'edit_url' => $postId > 0 ? (get_edit_post_link($postId, 'raw') ?: null) : null, 'site' => $siteId > 0 ? ($sites[$siteId] ?? ('#' . $siteId)) : '', 'action' => (string) $row['action_type'], 'status' => (string) $row['status'], 'user' => $authorNames[(int) $row['author_id']] ?? '', 'description' => $postId === 0 ? (string) ($row['error_message'] ?? '') : '', 'code' => (string) ($row['error_code'] ?? ''), 'message' => (string) ($row['error_message'] ?? ''), 'duration' => (float) ($row['response_time'] ?? 0), ]; }, $recent); } }