131 lines
5.3 KiB
PHP
131 lines
5.3 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
use App\Controllers\PublicController;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Web Designer task queue. The admin submits a design brief for any page; it
|
||
|
|
* lands in cja_tasks as `queued`. The console runner (agents/console/server.mjs)
|
||
|
|
* drains the queue: claims the oldest queued row, runs the design agent, builds,
|
||
|
|
* and auto-publishes (git commit), then marks it published/failed.
|
||
|
|
*
|
||
|
|
* POST /api/admintasks/create -> { title, target_page, prompt, draft, assets }
|
||
|
|
* GET /api/admintasks/list -> [{ task_id, title, status, ... }]
|
||
|
|
* GET /api/admintasks/get?id= -> full row + result
|
||
|
|
* POST /api/admintasks/cancel -> { id } (only if still queued)
|
||
|
|
*
|
||
|
|
* All admin-gated. This controller only does CRUD on the queue; the runner owns
|
||
|
|
* execution. The Node runner claims/finishes rows via api/cli/tasks-*.php.
|
||
|
|
*/
|
||
|
|
class AdminTasks extends PublicController
|
||
|
|
{
|
||
|
|
private const STATUSES = ['queued', 'running', 'published', 'failed', 'canceled'];
|
||
|
|
|
||
|
|
public function create(): void
|
||
|
|
{
|
||
|
|
$this->requireAdmin();
|
||
|
|
$this->guardPublic('admintasks_create', 60, 300);
|
||
|
|
|
||
|
|
$in = json_decode((string) file_get_contents('php://input'), true) ?: [];
|
||
|
|
|
||
|
|
$title = mb_substr(trim((string) ($in['title'] ?? '')), 0, 200);
|
||
|
|
$target = mb_substr(trim((string) ($in['target_page'] ?? '')), 0, 200);
|
||
|
|
$prompt = trim((string) ($in['prompt'] ?? ''));
|
||
|
|
$draft = (string) ($in['draft'] ?? '');
|
||
|
|
|
||
|
|
if ($prompt === '') {
|
||
|
|
$this->json(null, 400, ['code' => 'no_prompt', 'message' => 'Describe what you want done.']);
|
||
|
|
}
|
||
|
|
if ($target === '') {
|
||
|
|
$this->json(null, 400, ['code' => 'no_target', 'message' => 'Pick a page (or a new page) for the task.']);
|
||
|
|
}
|
||
|
|
if ($title === '') {
|
||
|
|
$title = mb_substr($prompt, 0, 80);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Assets: only known-safe local /media/ images and real http(s) video URLs.
|
||
|
|
$images = [];
|
||
|
|
foreach ((array) ($in['assets']['images'] ?? []) as $img) {
|
||
|
|
$url = trim((string) ($img['url'] ?? ''));
|
||
|
|
if (preg_match('#^/media/[\w./-]+$#', $url)) {
|
||
|
|
$images[] = ['url' => $url, 'alt' => mb_substr(trim((string) ($img['alt'] ?? '')), 0, 200)];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$videos = [];
|
||
|
|
foreach ((array) ($in['assets']['videos'] ?? []) as $v) {
|
||
|
|
$url = trim((string) $v);
|
||
|
|
if ($url !== '' && filter_var($url, FILTER_VALIDATE_URL) && preg_match('#^https?://#i', $url)) {
|
||
|
|
$videos[] = mb_substr($url, 0, 500);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$assets = json_encode(['images' => $images, 'videos' => $videos], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||
|
|
|
||
|
|
$order = (int) \Db::getValue('SELECT COALESCE(MAX(sort_order), 0) + 10 FROM cja_tasks');
|
||
|
|
\Db::insert('cja_tasks', [
|
||
|
|
'title' => $title,
|
||
|
|
'target_page' => $target,
|
||
|
|
'prompt' => $prompt,
|
||
|
|
'draft' => $draft,
|
||
|
|
'assets' => $assets,
|
||
|
|
'status' => 'queued',
|
||
|
|
'sort_order' => $order,
|
||
|
|
]);
|
||
|
|
|
||
|
|
$this->json(['ok' => true, 'id' => (int) \Db::getValue('SELECT LAST_INSERT_ID()')]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function list(): void
|
||
|
|
{
|
||
|
|
$this->requireAdmin();
|
||
|
|
$rows = \Db::select(
|
||
|
|
'SELECT task_id, title, target_page, status, result, created_at, finished_at
|
||
|
|
FROM cja_tasks ORDER BY sort_order DESC, task_id DESC LIMIT 100'
|
||
|
|
);
|
||
|
|
$out = array_map(function ($r) {
|
||
|
|
$result = json_decode($r['result'] ?? '', true) ?: [];
|
||
|
|
return [
|
||
|
|
'id' => (int) $r['task_id'],
|
||
|
|
'title' => $r['title'],
|
||
|
|
'target' => $r['target_page'],
|
||
|
|
'status' => $r['status'],
|
||
|
|
'commit' => $result['commit'] ?? '',
|
||
|
|
'summary' => $result['summary'] ?? '',
|
||
|
|
'live' => $result['live'] ?? '',
|
||
|
|
'error' => $result['error'] ?? '',
|
||
|
|
'created' => $r['created_at'],
|
||
|
|
'finished' => $r['finished_at'],
|
||
|
|
];
|
||
|
|
}, $rows);
|
||
|
|
$this->json(['tasks' => $out]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function get(): void
|
||
|
|
{
|
||
|
|
$this->requireAdmin();
|
||
|
|
$id = (int) ($_GET['id'] ?? 0);
|
||
|
|
$r = \Db::getRow('SELECT * FROM cja_tasks WHERE task_id = ?', [$id]);
|
||
|
|
if (!$r) {
|
||
|
|
$this->json(null, 404, ['code' => 'not_found', 'message' => 'No such task.']);
|
||
|
|
}
|
||
|
|
$r['assets'] = json_decode($r['assets'] ?? '{}', true) ?: [];
|
||
|
|
$r['result'] = json_decode($r['result'] ?? '{}', true) ?: [];
|
||
|
|
$this->json($r);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Cancel a task that has not started yet. */
|
||
|
|
public function cancel(): void
|
||
|
|
{
|
||
|
|
$this->requireAdmin();
|
||
|
|
$in = json_decode((string) file_get_contents('php://input'), true) ?: [];
|
||
|
|
$id = (int) ($in['id'] ?? 0);
|
||
|
|
$status = (string) \Db::getValue('SELECT status FROM cja_tasks WHERE task_id = ?', [$id]);
|
||
|
|
if ($status === '') {
|
||
|
|
$this->json(null, 404, ['code' => 'not_found', 'message' => 'No such task.']);
|
||
|
|
}
|
||
|
|
if ($status !== 'queued') {
|
||
|
|
$this->json(null, 409, ['code' => 'not_queued', 'message' => 'Only queued tasks can be canceled.']);
|
||
|
|
}
|
||
|
|
\Db::update('cja_tasks', ['status' => 'canceled', 'finished_at' => date('Y-m-d H:i:s')], 'task_id = ?', [$id]);
|
||
|
|
$this->json(['ok' => true]);
|
||
|
|
}
|
||
|
|
}
|