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 '';
}
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 '
';
echo '';
echo ' ';
echo '' . esc_html__('Filters', '1platform-content-ai') . ' ';
if ($activeCount > 0) {
echo '' . esc_html($activeCount) . ' ';
}
echo '
';
echo '';
echo ' ';
echo '
';
echo ' ';
// Filter body
echo '
'; // end filter body
echo '
'; // end filters container
}
private function renderTable(): void {
echo '';
echo '
';
echo '
';
// Header — consolidated columns
echo '';
echo '' . esc_html__('Status', '1platform-content-ai') . ' ';
echo '' . esc_html__('Request', '1platform-content-ai') . ' ';
echo '' . esc_html__('Module', '1platform-content-ai') . ' ';
echo '' . esc_html__('Message', '1platform-content-ai') . ' ';
echo '' . esc_html__('Source', '1platform-content-ai') . ' ';
echo '' . esc_html__('Time', '1platform-content-ai') . ' ';
echo ' ';
echo ' ';
echo '';
foreach ($this->logData['items'] as $item) {
$this->renderTableRow($item);
}
echo ' ';
echo '
';
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 '';
// 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 '' . esc_html($key) . ' ' . esc_html($detail['request_context'][$key]) . ' ';
}
}
echo '
';
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 '';
}
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 {
?>