0.
$run_raw = filter_input(INPUT_GET, 'run_id', FILTER_UNSAFE_RAW);
$run_id = is_scalar($run_raw) ? absint(wp_unslash((string) $run_raw)) : 0;
if ($run_id <= 0) {
$latest_audit = $repo->get_latest_completed_run('audit');
if ($latest_audit) {
$run_id = (int) $latest_audit['id'];
}
}
$category_raw = filter_input(INPUT_GET, 'category', FILTER_UNSAFE_RAW);
$severity_raw = filter_input(INPUT_GET, 'severity', FILTER_UNSAFE_RAW);
$status_raw = filter_input(INPUT_GET, 'status', FILTER_UNSAFE_RAW);
$group_raw = filter_input(INPUT_GET, 'group', FILTER_UNSAFE_RAW);
$paged_raw = filter_input(INPUT_GET, 'paged', FILTER_UNSAFE_RAW);
$category = is_string($category_raw) ? sanitize_text_field(wp_unslash($category_raw)) : '';
$severity = is_string($severity_raw) ? sanitize_key(wp_unslash($severity_raw)) : '';
$status = is_string($status_raw) ? sanitize_key(wp_unslash($status_raw)) : '';
$group = is_string($group_raw) ? sanitize_key(wp_unslash($group_raw)) : '';
if (!in_array($group, ['fix', 'improve', 'note'], true)) {
$group = '';
}
$paged = is_scalar($paged_raw) ? max(1, absint(wp_unslash((string) $paged_raw))) : 1;
$per_page = 30;
echo wp_kses(Brand::wrap_open(), Brand::admin_kses());
echo wp_kses(Brand::header(I18n::tr('Findings', 'النتائج')), Brand::admin_kses());
$all = $run_id > 0 ? $repo->get_findings_for_run($run_id) : [];
if (empty($all)) {
$this->render_empty_state($run_id);
echo '';
return;
}
// Counts per action group, over the whole run.
$group_counts = ['fix' => 0, 'improve' => 0, 'note' => 0];
foreach ($all as $row) {
if (!$this->is_handled($row)) {
$group_counts[Labels::group_of((string) ($row['severity'] ?? 'info'))]++;
}
}
// Apply filters.
$rows = array_values(array_filter($all, function (array $row) use ($category, $severity, $status, $group): bool {
if ($category !== '' && (string) ($row['category'] ?? '') !== $category) {
return false;
}
if ($severity !== '' && strtolower((string) ($row['severity'] ?? '')) !== $severity) {
return false;
}
if ($status !== '' && strtolower((string) ($row['status'] ?? '')) !== $status) {
return false;
}
if ($group !== '' && Labels::group_of((string) ($row['severity'] ?? 'info')) !== $group) {
return false;
}
return true;
}));
$base = admin_url('admin.php?page=aetos-seo-findings&run_id=' . (int) $run_id);
$this->render_group_chips($base, $group, $group_counts);
$previous_run_id = $repo->previous_completed_audit_run_id($run_id);
$auto_resolved = $previous_run_id > 0 ? $repo->count_findings(['run_id' => $previous_run_id, 'status' => 'done']) : 0;
if ($auto_resolved > 0) {
echo '
'
. esc_html(sprintf(I18n::tr('Fixed since the previous audit: %d', 'المحلولة منذ الفحص السابق: %d'), (int) $auto_resolved))
. '
';
}
$this->render_filters($run_id, $category, $status);
echo '
';
if (empty($rows)) {
echo '' . esc_html(I18n::tr('No findings match these filters.', 'لا توجد نتائج مطابقة لهذه التصفية.')) . '
';
$this->render_modal();
echo '';
return;
}
// Main list: open work (fix, then improve). Notes and handled findings
// are folded below unless the user filtered for them explicitly.
$explicit = $status !== '' || $group === 'note' || $severity === 'info';
$main = [];
$notes = [];
$handled = [];
foreach ($rows as $row) {
if (!$explicit && $this->is_handled($row)) {
$handled[] = $row;
} elseif (!$explicit && Labels::group_of((string) ($row['severity'] ?? 'info')) === 'note') {
$notes[] = $row;
} else {
$main[] = $row;
}
}
$total_pages = max(1, (int) ceil(count($main) / $per_page));
$paged = min($paged, $total_pages);
$page_rows = array_slice($main, ($paged - 1) * $per_page, $per_page);
if (!empty($page_rows)) {
echo '';
foreach ($page_rows as $row) {
$this->render_card($row);
}
echo '
';
$this->render_pagination($paged, $total_pages);
} elseif (!$explicit) {
echo '' . esc_html(I18n::tr('Nothing needs fixing in this audit.', 'لا توجد مشكلات تحتاج إصلاحًا في هذا الفحص.')) . '
';
}
if (!empty($notes)) {
echo '' . esc_html(sprintf(I18n::tr('Notes for context (%d)', 'ملاحظات للعلم (%d)'), count($notes))) . ' ';
echo '' . esc_html(I18n::tr('These do not lower the score much or at all. Read them when you have time.', 'هذه الملاحظات لا تؤثر في النتيجة أو تؤثر قليلًا. راجعها حين يتوفر لديك وقت.')) . '
';
echo '';
foreach ($notes as $row) {
$this->render_card($row);
}
echo '
';
}
if (!empty($handled)) {
echo '' . esc_html(sprintf(I18n::tr('Fixed or ignored (%d)', 'تم حلها أو تجاهلها (%d)'), count($handled))) . ' ';
echo '';
foreach ($handled as $row) {
$this->render_card($row);
}
echo '
';
}
$this->render_modal();
echo '';
}
/**
* @param array $row
*/
private function is_handled(array $row): bool
{
return in_array(strtolower((string) ($row['status'] ?? 'open')), ['done', 'ignored'], true);
}
private function render_empty_state(int $run_id): void
{
$dashboard_url = admin_url('admin.php?page=aetos-seo-dashboard');
echo '';
echo '
';
echo '
' . esc_html($run_id > 0 ? I18n::tr('This audit found no issues', 'لم يجد هذا الفحص أي مشكلات') : I18n::tr('No audit yet', 'لم يُشغَّل أي فحص بعد')) . ' ';
echo '
' . esc_html(I18n::tr(
'Start an audit from the dashboard. Findings appear here, ordered by what to fix first.',
'ابدأ فحصًا من لوحة التحكم. ستظهر النتائج هنا مرتبة حسب الأهم إصلاحًا.'
)) . '
';
echo '
' . esc_html(I18n::tr('Go to the dashboard', 'الذهاب إلى لوحة التحكم')) . ' ';
echo '
';
}
/**
* @param array $counts
*/
private function render_group_chips(string $base, string $active, array $counts): void
{
echo '';
$items = [
'' => array_sum($counts),
'fix' => $counts['fix'],
'improve' => $counts['improve'],
'note' => $counts['note'],
];
foreach ($items as $key => $count) {
$href = $key === '' ? $base : add_query_arg('group', $key, $base);
$cls = 'aetos-group-chip aetos-group-chip-' . ($key === '' ? 'all' : $key) . ($active === $key ? ' is-active' : '');
echo '';
echo '' . esc_html((string) (int) $count) . ' ';
echo '' . esc_html(Labels::group($key)) . ' ';
echo ' ';
}
echo ' ';
}
private function render_filters(int $run_id, string $category, string $status): void
{
echo '' . esc_html(I18n::tr('Search and filter', 'بحث وتصفية')) . ' ';
echo ' ';
}
/**
* @param array $row
*/
private function render_card(array $row): void
{
$data = $this->finding_payload($row);
$row_id = (int) ($row['id'] ?? 0);
$sev = (string) ($data['severity'] ?? 'info');
$status = strtolower((string) ($data['status'] ?? 'open'));
$why = $this->text_excerpt((string) ($data['why_it_matters'] ?? ''), 180);
$data['ai_prompt'] = \AetosSeo\Utils\IssuePrompt::build($data);
$data['row_id'] = $row_id;
$data['is_handled'] = $this->is_handled($row);
echo '';
echo '';
echo '' . esc_html(Labels::severity($sev)) . ' ';
echo '' . esc_html(CategoryLabels::label((string) ($data['category'] ?? ''))) . ' ';
if ($status !== 'open') {
echo '' . esc_html(Labels::status($status)) . ' ';
} else {
echo '' . esc_html(Labels::status('open')) . ' ';
}
echo '
';
echo '' . esc_html((string) ($data['title'] ?? '')) . ' ';
if ($why !== '') {
echo '' . esc_html($why) . '
';
}
echo '';
echo '' . esc_html(I18n::tr('Details and fix', 'التفاصيل وطريقة الحل')) . ' ';
if (!$this->is_handled($row)) {
echo '' . esc_html(I18n::tr('Mark as fixed', 'تم حلها')) . ' ';
echo '' . esc_html(I18n::tr('Ignore', 'تجاهل')) . ' ';
} else {
echo '' . esc_html(I18n::tr('Reopen', 'إعادة فتح')) . ' ';
}
echo '
';
echo ' ';
}
private function render_pagination(int $paged, int $total_pages): void
{
if ($total_pages <= 1) {
return;
}
$base_url = remove_query_arg('paged');
echo '';
}
private function render_modal(): void
{
echo '';
echo '
';
echo '
' . esc_html(I18n::tr('Close', 'إغلاق')) . ' ';
echo '
';
echo '
';
}
/**
* @param array $row
* @return array
*/
private function finding_payload(array $row): array
{
$evidence = $this->decode_payload($row['evidence'] ?? null, []);
$rec_steps = $this->decode_payload($row['recommendation_steps'] ?? null, []);
$retest = $this->decode_payload($row['retest'] ?? null, []);
$finding_id = (string) ($row['finding_id'] ?? '');
$category = (string) ($row['category'] ?? '');
$fallback_title = $this->fallback_finding_title($finding_id, $category);
return [
'id' => $finding_id,
'title' => I18n::english_safe((string) ($row['title'] ?? ''), $fallback_title),
'category' => $category,
'severity' => (string) ($row['severity'] ?? ''),
'status' => (string) ($row['status'] ?? ''),
// 4.0.17: the evidence modal printed the raw status and category keys
// ("OPEN", "AI-Friendly") inside the Arabic interface.
'status_label' => $this->status_label((string) ($row['status'] ?? 'open')),
'category_label' => $category !== '' ? CategoryLabels::label($category) : '',
'score_impact' => (int) ($row['score_impact'] ?? 0),
'evidence' => $evidence,
'why_it_matters' => I18n::english_safe(
(string) ($row['why_it_matters'] ?? ''),
I18n::tr('Run a fresh audit in English to regenerate the full explanation for this stored finding.', 'أعد تشغيل الفحص بالعربية لتحديث شرح هذه النتيجة المخزنة.')
),
'recommendation_steps' => $this->english_safe_steps($rec_steps),
'retest' => $this->english_safe_retest($retest),
];
}
private function fallback_finding_title(string $finding_id, string $category): string
{
$category_label = $category !== '' ? CategoryLabels::label($category) : I18n::tr('Audit', 'الفحص');
$finding_label = $finding_id !== '' ? $finding_id : I18n::tr('stored finding', 'نتيجة مخزنة');
return sprintf(I18n::tr('%1$s issue: %2$s', 'مشكلة %1$s: %2$s'), $category_label, $finding_label);
}
/**
* @param array $steps
* @return array
*/
private function english_safe_steps(array $steps): array
{
if (I18n::is_arabic()) {
return $steps;
}
if ($this->contains_arabic_value($steps)) {
return [
I18n::tr('Run a fresh audit in English to regenerate the step-by-step fix for this stored finding.', 'أعد تشغيل الفحص بالعربية لتحديث خطوات إصلاح هذه النتيجة المخزنة.'),
];
}
return $steps;
}
/**
* @param array $retest
* @return array
*/
private function english_safe_retest(array $retest): array
{
if (I18n::is_arabic() || !$this->contains_arabic_value($retest)) {
return $retest;
}
return [
'how' => 'Run a fresh audit in English to regenerate the verification instructions for this stored finding.',
'pass_condition' => 'The new audit provides English verification instructions.',
];
}
/**
* @param mixed $value
*/
private function contains_arabic_value($value): bool
{
if (is_string($value)) {
return I18n::contains_arabic($value);
}
if (is_array($value)) {
foreach ($value as $key => $item) {
if ((is_string($key) && I18n::contains_arabic($key)) || $this->contains_arabic_value($item)) {
return true;
}
}
}
return false;
}
/**
* @param mixed $value
* @param array $fallback
* @return array
*/
private function decode_payload($value, array $fallback): array
{
if (is_array($value)) {
return $value;
}
if (!is_string($value) || trim($value) === '') {
return $fallback;
}
$decoded = json_decode($value, true);
return is_array($decoded) ? $decoded : $fallback;
}
private function severity_label(string $severity): string
{
return Labels::severity($severity);
}
private function status_label(string $status): string
{
return Labels::status($status);
}
private function text_excerpt(string $text, int $max): string
{
$text = trim(wp_strip_all_tags($text));
if ($text === '') {
return '';
}
return html_entity_decode(wp_html_excerpt($text, $max, '...'), ENT_QUOTES, 'UTF-8');
}
/**
* @return array
*/
private function categories(): array
{
return [
'Performance', 'Technical SEO', 'On-Page', 'AI-Friendly',
'Mobile Usability', 'Security', 'Database', 'Media', 'Crawl', 'Analytics',
];
}
}