service = ContaiLogsService::getInstance(); } public function render(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Unauthorized', '1platform-content-ai')); } $this->handleClearLogsAction(); $this->loadFilters(); $this->fetchLogs(); echo '
'; $this->renderPageHeader(); $this->renderNotices(); $this->renderPendingBufferBanner(); // Check if viewing detail $detail_id = isset($_GET['log_id']) ? sanitize_text_field(wp_unslash($_GET['log_id'])) : ''; if (!empty($detail_id)) { $this->renderDetailView($detail_id); } else { $this->renderStats(); $this->renderFilters(); if ($this->errorMessage) { $this->renderErrorState(); } elseif (empty($this->logData['items'])) { $this->renderEmptyState(); } else { $this->renderTable(); } } echo '
'; $this->renderInlineScript(); } private function handleClearLogsAction(): void { if (!isset($_POST['contai_clear_logs_action'])) { return; } if (!isset($_POST['contai_clear_logs_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['contai_clear_logs_nonce'])), 'contai_clear_logs')) { $this->errorMessage = 'Security check failed.'; return; } if (!current_user_can('manage_options')) { $this->errorMessage = 'Unauthorized.'; return; } $payload = []; if (!empty($_POST['clear_provider'])) { $payload['provider'] = sanitize_text_field(wp_unslash($_POST['clear_provider'])); } if (!empty($_POST['clear_website_id'])) { $payload['website_id'] = sanitize_text_field(wp_unslash($_POST['clear_website_id'])); } $response = $this->service->clearLogs($payload); if ($response->isSuccess()) { $data = $response->getData(); $count = $data['deleted_count'] ?? 0; $this->successMessage = sprintf('%d log(s) cleared successfully.', (int) $count); } else { $this->errorMessage = $response->getMessage() ?? 'Failed to clear logs.'; } } private function loadFilters(): void { $this->filters = [ 'website_id' => isset($_GET['website_id']) ? sanitize_text_field(wp_unslash($_GET['website_id'])) : '', 'provider' => isset($_GET['provider']) ? sanitize_text_field(wp_unslash($_GET['provider'])) : '', 'success' => isset($_GET['success']) ? sanitize_text_field(wp_unslash($_GET['success'])) : '', 'error_type' => isset($_GET['error_type']) ? sanitize_text_field(wp_unslash($_GET['error_type'])) : '', 'status_code' => isset($_GET['status_code']) ? absint($_GET['status_code']) : '', 'endpoint' => isset($_GET['endpoint']) ? sanitize_text_field(wp_unslash($_GET['endpoint'])) : '', 'source_type' => isset($_GET['source_type']) ? sanitize_text_field(wp_unslash($_GET['source_type'])) : '', 'trace_id' => isset($_GET['trace_id']) ? sanitize_text_field(wp_unslash($_GET['trace_id'])) : '', 'from_date' => isset($_GET['from_date']) ? sanitize_text_field(wp_unslash($_GET['from_date'])) : '', 'to_date' => isset($_GET['to_date']) ? sanitize_text_field(wp_unslash($_GET['to_date'])) : '', 'page' => isset($_GET['paged']) ? absint($_GET['paged']) : 1, 'page_size' => 20, ]; if (!empty($this->filters['error_type']) && !in_array($this->filters['error_type'], ContaiLogsAdapter::VALID_ERROR_TYPES, true)) { $this->filters['error_type'] = ''; } if (!empty($this->filters['source_type']) && !in_array($this->filters['source_type'], ['server', 'client'], true)) { $this->filters['source_type'] = ''; } if ($this->filters['success'] !== '' && !in_array($this->filters['success'], ['true', 'false'], true)) { $this->filters['success'] = ''; } } private function fetchLogs(): void { $response = $this->service->listLogs($this->filters); if ($response->isSuccess()) { $this->logData = ContaiLogsAdapter::fromListResponse($response); } else { $this->errorMessage = $response->getMessage() ?? 'Failed to fetch logs.'; $this->logData = ['items' => [], 'pagination' => ['page' => 1, 'page_size' => 20, 'total' => 0, 'total_pages' => 0]]; } } private function renderPageHeader(): void { echo '
'; echo '

' . esc_html__('Logs', '1platform-content-ai') . '

'; echo '

' . esc_html__('Monitor API requests, diagnose integration issues, and track errors across all modules.', '1platform-content-ai') . '

'; echo '
'; } private function renderNotices(): void { if (!empty($this->successMessage)) { echo '

' . esc_html($this->successMessage) . '

'; } } private function renderPendingBufferBanner(): void { $count = ContaiClientLogReporter::getBufferCount(); if ($count <= 0) { return; } echo '
'; echo '

'; printf( esc_html__('%d error(s) pending sync. These occurred while the backend was unreachable and will be synchronized automatically on the next successful connection.', '1platform-content-ai'), (int) $count ); echo '

'; } private function renderStats(): void { $pagination = $this->logData['pagination'] ?? ['total' => 0, 'page' => 1, 'total_pages' => 0]; $total = $pagination['total']; $page = $pagination['page']; $totalPages = $pagination['total_pages']; // Count errors/successes in current page $errors = 0; $successes = 0; foreach ($this->logData['items'] as $item) { if ($item['success']) { $successes++; } else { $errors++; } } echo '
'; // Total logs echo '
'; echo '
'; echo '
'; echo '
' . esc_html(number_format_i18n($total)) . '
'; echo '
' . esc_html__('Total Logs', '1platform-content-ai') . '
'; echo '
'; // Errors on page echo '
'; echo '
'; echo '
'; echo '
' . esc_html($errors) . '
'; echo '
' . esc_html__('Errors on Page', '1platform-content-ai') . '
'; echo '
'; // Successes on page echo '
'; echo '
'; echo '
'; echo '
' . esc_html($successes) . '
'; echo '
' . esc_html__('Success on Page', '1platform-content-ai') . '
'; echo '
'; // Current page echo '
'; echo '
'; echo '
'; echo '
' . esc_html($page) . ' / ' . esc_html($totalPages ?: 1) . '
'; echo '
' . esc_html__('Current Page', '1platform-content-ai') . '
'; echo '
'; echo '
'; } private function getActiveFilterCount(): int { $count = 0; $filterKeys = ['trace_id', 'provider', 'error_type', 'source_type', 'success', 'from_date', 'to_date', 'website_id']; foreach ($filterKeys as $key) { if (!empty($this->filters[$key])) { $count++; } } return $count; } private function renderFilters(): void { $baseUrl = admin_url('admin.php?page=contai-logs'); $activeCount = $this->getActiveFilterCount(); $isOpen = $activeCount > 0; echo '
'; // Toggle header echo ''; // Filter body echo '
'; echo '
'; echo ''; echo '
'; // Trace ID echo '
'; echo ''; echo ''; echo '
'; // Provider echo '
'; echo ''; echo ''; echo '
'; // Error Type echo '
'; echo ''; echo ''; echo '
'; // Source Type echo '
'; echo ''; echo ''; echo '
'; // Status echo '
'; echo ''; echo ''; echo '
'; // From Date echo '
'; echo ''; echo ''; echo '
'; // To Date echo '
'; echo ''; echo ''; echo '
'; // Website ID echo '
'; echo ''; echo ''; echo '
'; echo '
'; // end grid // Actions row echo '
'; echo '
'; echo ''; echo '' . esc_html__('Reset', '1platform-content-ai') . ''; echo '
'; // Clear Logs (right-aligned, subtle) echo ''; wp_nonce_field('contai_clear_logs', 'contai_clear_logs_nonce'); echo ''; if (!empty($this->filters['provider'])) { echo ''; } if (!empty($this->filters['website_id'])) { echo ''; } echo ''; echo ''; echo '
'; // end actions echo ''; echo '
'; // end filter body echo '
'; // end filters container } private function renderTable(): void { echo '
'; echo '
'; echo ''; // Header — consolidated columns echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ($this->logData['items'] as $item) { $this->renderTableRow($item); } echo ''; echo '
' . esc_html__('Status', '1platform-content-ai') . '' . esc_html__('Request', '1platform-content-ai') . '' . esc_html__('Module', '1platform-content-ai') . '' . esc_html__('Message', '1platform-content-ai') . '' . esc_html__('Source', '1platform-content-ai') . '' . esc_html__('Time', '1platform-content-ai') . '
'; echo '
'; // Pagination inside the card $this->renderPagination(); echo '
'; } private function renderTableRow(array $item): void { $rowClass = $item['success'] ? 'contai-row-success' : 'contai-row-error'; echo ''; // Status — combined HTTP code + result with dot indicator $badgeClass = ContaiLogsAdapter::getStatusBadgeClass($item['response_status']); $statusCode = $item['response_status'] > 0 ? $item['response_status'] : 'N/A'; $dotClass = $item['success'] ? 'contai-logs-status-dot--success' : 'contai-logs-status-dot--error'; echo ''; echo ''; echo '' . esc_html($statusCode) . ''; echo ''; // Request — method + endpoint stacked, trace ID below $method = strtoupper($item['method'] ?: 'GET'); $methodClass = 'contai-logs-method-' . strtolower($method); $endpoint = $item['endpoint'] ?: '—'; // Show only the path portion for readability $endpointPath = $endpoint; if (strpos($endpoint, '/api/') !== false) { $endpointPath = substr($endpoint, strpos($endpoint, '/api/')); } $endpointDisplay = strlen($endpointPath) > 45 ? substr($endpointPath, 0, 45) . '...' : $endpointPath; echo ''; echo '
' . esc_html($method) . ''; echo '' . esc_html($endpointDisplay) . '
'; if (!empty($item['trace_id'])) { echo '
' . esc_html($item['trace_id']) . '
'; } echo ''; // Module — provider + action stacked $provider = ContaiLogsAdapter::getProviderDisplayName($item['provider']); $action = !empty($item['action']) ? $item['action'] : ''; echo ''; echo '
' . esc_html($provider) . '
'; if ($action) { echo '
' . esc_html($action) . '
'; } echo ''; // Message $msg = $item['response_message'] ?: '—'; $msgDisplay = strlen($msg) > 50 ? substr($msg, 0, 50) . '...' : $msg; echo ''; if (!$item['success']) { $errorLabel = ContaiLogsAdapter::getErrorTypeLabel($item['error_type']); if (!empty($errorLabel)) { echo '' . esc_html($errorLabel) . '
'; } } echo '' . esc_html($msgDisplay) . ''; echo ''; // Source $sourceClass = ($item['source_type'] === 'client') ? 'contai-badge-client' : 'contai-badge-info'; echo '' . esc_html(ucfirst($item['source_type'])) . ''; // Time — relative-style display echo '' . esc_html($item['timestamp']) . ''; // Actions $detailUrl = admin_url('admin.php?page=contai-logs&log_id=' . urlencode($item['id'])); echo ''; echo ''; } private function renderDetailView(string $logId): void { $response = $this->service->getLogDetail($logId); if (!$response->isSuccess()) { echo '

' . esc_html($response->getMessage() ?? 'Log not found.') . '

'; echo ' ' . esc_html__('Back to Logs', '1platform-content-ai') . ''; return; } $detail = ContaiLogsAdapter::fromDetailResponse($response); // Back button echo ' ' . esc_html__('Back to Logs', '1platform-content-ai') . ''; echo '
'; // Detail header with status $dotClass = $detail['success'] ? 'contai-logs-status-dot--success' : 'contai-logs-status-dot--error'; $badgeVariant = $detail['success'] ? 'contai-badge-success' : 'contai-badge-danger'; $badgeLabel = $detail['success'] ? __( 'Success', '1platform-content-ai' ) : __( 'Error', '1platform-content-ai' ); $badgeClass = ContaiLogsAdapter::getStatusBadgeClass( $detail['response_status'] ); $statusDisplay = $detail['response_status'] > 0 ? $detail['response_status'] : 'N/A'; echo '
'; echo '
'; echo '

' . esc_html__('Log Detail', '1platform-content-ai') . '

'; echo '
'; echo ''; echo '' . esc_html( $badgeLabel ) . ''; echo '' . esc_html($statusDisplay) . ''; if (!empty($detail['trace_id'])) { echo '' . esc_html($detail['trace_id']) . ''; } echo '
'; // Info grid echo '
'; $this->renderDetailField('Timestamp', $detail['timestamp'] . (!empty($detail['timestamp_raw']) ? ' (' . $detail['timestamp_raw'] . ' UTC)' : '')); $this->renderDetailField('Method', $detail['method'] ?: '—'); $this->renderDetailField('Endpoint', $detail['endpoint'] ?: '—'); $this->renderDetailField('Action', $detail['action'] ?: '—'); $this->renderDetailField('Module', $detail['source_module'] ?: '—'); $this->renderDetailField('Provider', ContaiLogsAdapter::getProviderDisplayName($detail['provider'])); $this->renderDetailField('Website ID', $detail['website_id'] ?: '—'); $this->renderDetailField('Source Type', ucfirst($detail['source_type'])); $this->renderDetailField('Response Message', $detail['response_message'] ?: '—'); if (!empty($detail['error_type'])) { $this->renderDetailField('Error Type', ContaiLogsAdapter::getErrorTypeLabel($detail['error_type'])); } if ($detail['occurrences'] > 1) { $this->renderDetailField('Occurrences', (string) $detail['occurrences']); } echo '
'; // Request Payload if (!empty($detail['request_payload'])) { echo '
'; echo '

' . esc_html__('Request Payload', '1platform-content-ai') . '

'; echo '
' . esc_html(wp_json_encode($detail['request_payload'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) . '
'; echo '
'; } // Response Data if (!empty($detail['response_data'])) { echo '
'; echo '

' . esc_html__('Response Data', '1platform-content-ai') . '

'; echo '
' . esc_html(wp_json_encode($detail['response_data'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) . '
'; echo '
'; } // Client Context if (!empty($detail['request_context']) && $detail['source_type'] === 'client') { echo '
'; echo '

' . esc_html__('Client Context', '1platform-content-ai') . '

'; echo ''; $allowedKeys = ['wp_error_code', 'wp_error_message', 'php_version', 'wp_version', 'plugin_version']; foreach ($allowedKeys as $key) { if (isset($detail['request_context'][$key])) { echo ''; } } echo '
' . esc_html($key) . '' . esc_html($detail['request_context'][$key]) . '
'; echo '
'; } echo '
'; // end card } private function renderDetailField(string $label, string $value): void { echo '
'; echo '
' . esc_html($label) . '
'; echo '
' . esc_html($value) . '
'; echo '
'; } private function renderPagination(): void { $pagination = $this->logData['pagination']; if ($pagination['total'] === 0) { return; } $page = $pagination['page']; $totalPages = $pagination['total_pages']; $total = $pagination['total']; $pageSize = $pagination['page_size']; $from = (($page - 1) * $pageSize) + 1; $to = min($page * $pageSize, $total); echo '
'; echo ''; printf( esc_html__('Showing %1$d–%2$d of %3$d logs', '1platform-content-ai'), $from, $to, $total ); echo ''; echo '
'; if ($page > 1) { $prevUrl = $this->buildPaginationUrl($page - 1); echo '← ' . esc_html__('Previous', '1platform-content-ai') . ''; } if ($page < $totalPages) { $nextUrl = $this->buildPaginationUrl($page + 1); echo '' . esc_html__('Next', '1platform-content-ai') . ' →'; } echo '
'; } private function buildPaginationUrl(int $page): string { $params = ['page' => 'contai-logs', 'paged' => $page]; foreach (['website_id', 'provider', 'success', 'error_type', 'status_code', 'endpoint', 'source_type', 'trace_id', 'from_date', 'to_date'] as $key) { if (!empty($this->filters[$key])) { $params[$key] = $this->filters[$key]; } } return admin_url('admin.php?' . http_build_query($params)); } private function renderEmptyState(): void { echo '
'; echo '
'; echo '
'; echo '

' . esc_html__('No logs found', '1platform-content-ai') . '

'; echo '

' . esc_html__('No logs match the selected filters. Try adjusting your search criteria.', '1platform-content-ai') . '

'; echo '
'; } private function renderErrorState(): void { echo '
'; echo '

' . esc_html($this->errorMessage) . '

'; echo '
'; } private function renderInlineScript(): void { ?>