report = new HealthReport($plugin); } /** Output of markup built from escaped parts, integers and fixed labels, filtered once more through the admin allowlist (wp_kses). */ private function out(string $html): void { echo wp_kses($html, AdminView::allowedHtml()); // the markup is built from escaped parts; the allowlist is the second barrier } private static function when(int $t): string { return wp_date(get_option('date_format') . ' ' . get_option('time_format'), $t); } // ---------------------------------------------------------------- Overview public function overviewSummary(): void { if (!current_user_can(Capabilities::VIEW_USAGE)) { return; } $assistants = $this->report->assistants(); $items = array_filter($this->report->items(), static fn(array $i): bool => $i['severity'] !== 'info'); $this->out('

' . esc_html__('Public assistants: what the site’s records show', 'aiwend') . '

'); if ($assistants === []) { $this->out('

' . esc_html__('No assistant is published yet.', 'aiwend') . '

'); } else { $this->out(''); } $this->out('

' . esc_html__('This comes from the site’s own settings and records. Aiwend did not contact the AI provider to produce it, so “configured” is not a promise that answers are working, and “recent failure” means a request failed in the last day with no successful answer since.', 'aiwend') . '

'); if ($items !== []) { $this->out('

' . esc_html(sprintf(/* translators: %d: number of items */ _n('%d thing needs attention.', '%d things need attention.', count($items), 'aiwend'), count($items))) . ' ' . esc_html__('Open diagnostics', 'aiwend') . '

'); } $this->out('
'); } // ---------------------------------------------------------------- Diagnostics public function needsAttention(): void { if (!current_user_can(Capabilities::VIEW_USAGE)) { return; } $items = $this->report->items(); $this->out('

' . esc_html__('Needs attention', 'aiwend') . '

'); $this->out('

' . esc_html__('Built from the site’s own records. Nothing here is a live test of your AI provider, and no provider was contacted to show it.', 'aiwend') . '

'); if ($items === []) { $this->out('

' . esc_html__('Nothing needs attention according to the site’s records.', 'aiwend') . '

'); } foreach ($items as $item) { $this->item($item); } $this->repairStatus(); $this->deadJobs(); $this->out('
'); } /** @param array $item */ private function item(array $item): void { $d = FailureCatalog::describe((string) $item['category'], (string) $item['billing']); $tone = ['danger' => 'danger', 'warn' => 'warn', 'info' => 'neutral'][$item['severity']] ?? 'neutral'; $label = ['danger' => __('Problem', 'aiwend'), 'warn' => __('Note', 'aiwend'), 'info' => __('Earlier', 'aiwend')][$item['severity']] ?? ''; $line = $d['title'] . ($item['subject'] !== null ? ' — ' . $item['subject'] : ''); $this->out('
' . AdminView::badge($label, $tone) . ' ' . esc_html($line) . ' ' . esc_html((string) $item['component']) . ''); $when = ''; if (!empty($item['last'])) { $when = sprintf(/* translators: 1: date and time of the latest occurrence */ __('Latest: %s.', 'aiwend'), self::when((int) $item['last'])); if (!empty($item['since']) && (int) $item['since'] < (int) $item['last']) { $when .= ' ' . sprintf(/* translators: %s: date and time of the first occurrence in the last seven days */ __('First seen in the last seven days: %s.', 'aiwend'), self::when((int) $item['since'])); } } $this->out('
' . esc_html__('What is known', 'aiwend') . '
' . esc_html(trim($d['known'] . ' ' . $when)) . '
' . '
' . esc_html__('What might be wrong', 'aiwend') . '
' . esc_html($d['possible']) . '
' . '
' . esc_html__('Temporary?', 'aiwend') . '
' . esc_html($d['temporary_text']) . '
' . '
' . esc_html__('Was it charged?', 'aiwend') . '
' . esc_html($d['billing_text']) . '
' . '
' . esc_html__('What Aiwend does by itself', 'aiwend') . '
' . esc_html($d['auto']) . '
' . '
' . esc_html__('What you can do', 'aiwend') . '
' . esc_html($d['action']) . '
'); if ($item['link'] !== null) { $this->out('

' . esc_html($item['link'][0]) . '

'); } if ($item['action'] !== null) { $this->postButton($item['action']['do'], $item['action']['label'], $item['action']['fields'], 'aiwend-diagnostics'); } $this->out('
'); } /** @param array $fields */ private function postButton(string $do, string $label, array $fields, string $back, string $accessibleName = ''): void { ob_start(); AdminView::form($do, $back, $fields); echo ''; $this->out((string) ob_get_clean()); } private function repairStatus(): void { $last = $this->report->lastRepair(); $text = $last === null ? __('The daily check for knowledge sources whose indexing was never queued has not run yet.', 'aiwend') : sprintf(/* translators: 1: date and time, 2: number of sources */ _n('The daily check for knowledge sources whose indexing was never queued last ran on %1$s and queued %2$d source again.', 'The daily check for knowledge sources whose indexing was never queued last ran on %1$s and queued %2$d sources again.', $last['count'], 'aiwend'), self::when($last['at']), $last['count']); $this->out('

' . esc_html($text) . '

'); if (current_user_can(Capabilities::MANAGE_KNOWLEDGE)) { $this->postButton('run_repair', __('Run this check now', 'aiwend'), [], 'aiwend-diagnostics'); } } private function deadJobs(): void { $jobs = $this->report->deadJobs(); if ($jobs === []) { return; } $d = FailureCatalog::describe(FailureCatalog::JOB_FAILED); $this->out('

' . esc_html__('Failed background tasks', 'aiwend') . '

' . esc_html($d['known'] . ' ' . $d['auto']) . '

'); $this->out('
'); foreach ($jobs as $j) { $this->out(''); } $this->out('
' . esc_html__('Task', 'aiwend') . '' . esc_html__('Stopped', 'aiwend') . '' . esc_html__('Attempts', 'aiwend') . '' . esc_html__('Error', 'aiwend') . '' . esc_html__('Action', 'aiwend') . '
' . esc_html($j['label']) . '' . esc_html(self::when($j['updated_at'])) . '' . esc_html($j['attempts'] . '/' . $j['max_attempts']) . '' . esc_html($j['error'] === '' ? '—' : $j['error']) . ''); if ($j['retryable'] && current_user_can(Capabilities::MANAGE)) { $this->postButton('retry_job', __('Retry', 'aiwend'), ['job' => (string) $j['id']], 'aiwend-diagnostics', sprintf(/* translators: 1: task name, 2: task number */ __('Retry task: %1$s (number %2$d)', 'aiwend'), $j['label'], $j['id'])); } elseif (!$j['retryable']) { $this->out('' . esc_html($j['type'] === 'knowledge.ingest' ? __('Use Re-index on the source in Knowledge: indexing may contact an embedding service.', 'aiwend') : __('Not offered here: this kind of task is not safe to repeat from a button.', 'aiwend')) . ''); } $this->out('
'); } // ---------------------------------------------------------------- A saved connection /** Observed failures of one saved connection, kept apart from its saved configuration and its access check. */ public function connectionFacts(int $connectionId): void { if (!current_user_can(Capabilities::VIEW_USAGE)) { return; } $facts = $this->plugin->connectionHealth()->recent($connectionId); $success = (new \Aiwend\Usage\ProviderBreaker($this->plugin->db, $this->plugin->clock, $this->plugin->options))->lastSuccessAt(get_current_blog_id(), $connectionId); $this->out('

' . esc_html__('What live requests have met', 'aiwend') . '

'); $this->out('

' . esc_html__('Separate from the saved key and the access check above: this is what real visitor requests through this connection ran into in the last seven days. Nothing here changes the connection.', 'aiwend') . '

'); if ($facts === []) { $this->out('

' . esc_html__('No failure observed in the last seven days.', 'aiwend') . '

'); } foreach ($facts as $f) { $d = FailureCatalog::describe($f['category'], $f['billing']); $this->out('
$success ? ' open' : '') . '>' . esc_html($d['title']) . ' ' . esc_html(sprintf(/* translators: %s: date and time */ __('latest %s', 'aiwend'), self::when($f['last']))) . '' . '
' . esc_html__('What is known', 'aiwend') . '
' . esc_html($d['known']) . '
' . esc_html__('What might be wrong', 'aiwend') . '
' . esc_html($d['possible']) . '
' . '
' . esc_html__('Temporary?', 'aiwend') . '
' . esc_html($d['temporary_text']) . '
' . esc_html__('Was it charged?', 'aiwend') . '
' . esc_html($d['billing_text']) . '
' . '
' . esc_html__('What Aiwend does by itself', 'aiwend') . '
' . esc_html($d['auto']) . '
' . esc_html__('What you can do', 'aiwend') . '
' . esc_html($d['action']) . '
'); } if ($success > 0) { $this->out('

' . esc_html(sprintf(/* translators: %s: date and time */ __('The last answer through this connection was recorded on %s.', 'aiwend'), self::when($success))) . '

'); } $this->out('
'); } /** One short badge for the list of connections: observed health, not saved configuration. */ public function connectionBadge(int $connectionId): string { $facts = array_filter($this->plugin->connectionHealth()->recent($connectionId), static fn(array $f): bool => $f['category'] !== FailureCatalog::NO_ANSWER); if ($facts === []) { return '' . esc_html__('No recent failure observed', 'aiwend') . ''; } $success = (new \Aiwend\Usage\ProviderBreaker($this->plugin->db, $this->plugin->clock, $this->plugin->options))->lastSuccessAt(get_current_blog_id(), $connectionId); $latest = $facts[array_key_first($facts)]; return $latest['last'] > $success ? AdminView::badge(__('Recent failure observed', 'aiwend'), 'warn') : '' . esc_html__('Failures earlier; answered since', 'aiwend') . ''; } }