seedproject-web/api/public/controllers/adminqa.php

127 lines
5.5 KiB
PHP
Raw Normal View History

<?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]
: '/';
// Optional human instructions override the templated fix hint — this is
// the admin's "tell the Web Designer how to handle it" path.
$instructions = trim((string) ($in['instructions'] ?? ''));
if ($instructions !== '') {
$prompt = $instructions . "\n\n(QA context: a {$f['check_type']} issue on {$url}{$f['detail']})";
} else {
$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]);
}
}