120 lines
5.1 KiB
PHP
120 lines
5.1 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
use App\Controllers\PublicController;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* QA reports for the admin. The console runner performs the crawl and writes
|
||
|
|
* cja_qa_runs / cja_qa_findings; this controller reads them and, on request,
|
||
|
|
* turns a finding into a Web Designer fix task (cja_tasks) — the two queues
|
||
|
|
* chain, so a one-click fix flows through the existing build-gated designer.
|
||
|
|
*
|
||
|
|
* GET /api/adminqa/runs -> recent runs + counts
|
||
|
|
* GET /api/adminqa/findings?run=ID -> findings for a run (default latest)
|
||
|
|
* POST /api/adminqa/fix { finding_id } -> queue a Web Designer fix task
|
||
|
|
* POST /api/adminqa/ignore { finding_id } -> mark a finding ignored
|
||
|
|
*
|
||
|
|
* All admin-gated. The QA crawl itself is kicked via POST /devconsole/qa/run.
|
||
|
|
*/
|
||
|
|
class AdminQa extends PublicController
|
||
|
|
{
|
||
|
|
public function runs(): void
|
||
|
|
{
|
||
|
|
$this->requireAdmin();
|
||
|
|
$rows = \Db::select(
|
||
|
|
'SELECT run_id, status, `trigger`, summary, counts, started_at, finished_at
|
||
|
|
FROM cja_qa_runs ORDER BY run_id DESC LIMIT 30'
|
||
|
|
);
|
||
|
|
$out = array_map(function ($r) {
|
||
|
|
return [
|
||
|
|
'id' => (int) $r['run_id'],
|
||
|
|
'status' => $r['status'],
|
||
|
|
'trigger' => $r['trigger'],
|
||
|
|
'summary' => $r['summary'] ?? '',
|
||
|
|
'counts' => json_decode($r['counts'] ?? '{}', true) ?: [],
|
||
|
|
'started' => $r['started_at'],
|
||
|
|
'finished' => $r['finished_at'],
|
||
|
|
];
|
||
|
|
}, $rows);
|
||
|
|
$this->json(['runs' => $out]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function findings(): void
|
||
|
|
{
|
||
|
|
$this->requireAdmin();
|
||
|
|
$runId = (int) ($_GET['run'] ?? 0);
|
||
|
|
if ($runId <= 0) {
|
||
|
|
$runId = (int) \Db::getValue("SELECT run_id FROM cja_qa_runs WHERE status = 'done' ORDER BY run_id DESC LIMIT 1");
|
||
|
|
}
|
||
|
|
if ($runId <= 0) { $this->json(['run' => 0, 'findings' => []]); }
|
||
|
|
|
||
|
|
$rows = \Db::select(
|
||
|
|
"SELECT finding_id, check_type, severity, url, detail, status
|
||
|
|
FROM cja_qa_findings WHERE run_id = ?
|
||
|
|
ORDER BY FIELD(severity,'error','warning','info'), check_type, finding_id",
|
||
|
|
[$runId]
|
||
|
|
);
|
||
|
|
$out = array_map(fn($r) => [
|
||
|
|
'id' => (int) $r['finding_id'],
|
||
|
|
'check' => $r['check_type'],
|
||
|
|
'severity' => $r['severity'],
|
||
|
|
'url' => $r['url'],
|
||
|
|
'detail' => $r['detail'],
|
||
|
|
'status' => $r['status'],
|
||
|
|
], $rows);
|
||
|
|
$this->json(['run' => $runId, 'findings' => $out]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Turn a finding into a queued Web Designer fix task. */
|
||
|
|
public function fix(): void
|
||
|
|
{
|
||
|
|
$this->requireAdmin();
|
||
|
|
$this->guardPublic('adminqa_fix', 60, 300);
|
||
|
|
|
||
|
|
$in = json_decode((string) file_get_contents('php://input'), true) ?: [];
|
||
|
|
$fid = (int) ($in['finding_id'] ?? 0);
|
||
|
|
$f = \Db::getRow('SELECT * FROM cja_qa_findings WHERE finding_id = ?', [$fid]);
|
||
|
|
if (!$f) { $this->json(null, 404, ['code' => 'not_found', 'message' => 'No such finding.']); }
|
||
|
|
if ($f['status'] === 'fix_queued') { $this->json(null, 409, ['code' => 'already', 'message' => 'A fix is already queued for this finding.']); }
|
||
|
|
|
||
|
|
// Best-effort target page: a clean root-relative path, else the homepage.
|
||
|
|
// The prompt (fix_hint) carries the specific instruction + file/URL.
|
||
|
|
$url = (string) $f['url'];
|
||
|
|
$target = (str_starts_with($url, '/') && !str_contains($url, ',') && !str_contains($url, '://'))
|
||
|
|
? explode('#', explode('?', $url)[0])[0]
|
||
|
|
: '/';
|
||
|
|
$prompt = $f['fix_hint'] !== null && $f['fix_hint'] !== ''
|
||
|
|
? (string) $f['fix_hint']
|
||
|
|
: "QA found a {$f['check_type']} issue on {$url}: {$f['detail']}. Investigate and fix it.";
|
||
|
|
$title = 'QA fix: ' . $f['check_type'] . ' — ' . mb_substr($url, 0, 60);
|
||
|
|
|
||
|
|
$order = (int) \Db::getValue('SELECT COALESCE(MAX(sort_order), 0) + 10 FROM cja_tasks');
|
||
|
|
\Db::insert('cja_tasks', [
|
||
|
|
'title' => mb_substr($title, 0, 200),
|
||
|
|
'target_page' => mb_substr($target, 0, 200),
|
||
|
|
'prompt' => $prompt,
|
||
|
|
'draft' => '',
|
||
|
|
'assets' => json_encode(['images' => [], 'videos' => []]),
|
||
|
|
'status' => 'queued',
|
||
|
|
'sort_order' => $order,
|
||
|
|
]);
|
||
|
|
$taskId = (int) \Db::getValue('SELECT LAST_INSERT_ID()');
|
||
|
|
|
||
|
|
\Db::update('cja_qa_findings', ['status' => 'fix_queued'], 'finding_id = ?', [$fid]);
|
||
|
|
|
||
|
|
// The admin page kicks POST /devconsole/tasks/run after this returns.
|
||
|
|
$this->json(['ok' => true, 'task_id' => $taskId]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function ignore(): void
|
||
|
|
{
|
||
|
|
$this->requireAdmin();
|
||
|
|
$in = json_decode((string) file_get_contents('php://input'), true) ?: [];
|
||
|
|
$fid = (int) ($in['finding_id'] ?? 0);
|
||
|
|
if (!\Db::getValue('SELECT finding_id FROM cja_qa_findings WHERE finding_id = ?', [$fid])) {
|
||
|
|
$this->json(null, 404, ['code' => 'not_found', 'message' => 'No such finding.']);
|
||
|
|
}
|
||
|
|
\Db::update('cja_qa_findings', ['status' => 'ignored'], 'finding_id = ?', [$fid]);
|
||
|
|
$this->json(['ok' => true]);
|
||
|
|
}
|
||
|
|
}
|