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]); } }