diff --git a/api/cli/tasks-finish.php b/api/cli/tasks-finish.php new file mode 100644 index 0000000..58f2f05 --- /dev/null +++ b/api/cli/tasks-finish.php @@ -0,0 +1,42 @@ + --status=published|failed --result=\n"); + exit(1); +} + +// Validate the result payload is JSON; fall back to empty object if not. +if (json_decode($result) === null && json_last_error() !== JSON_ERROR_NONE) { + $result = '{}'; +} + +$pdo = new PDO( + sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', DB_HOST, DB_NAME), + DB_USER, + DB_PASS, + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] +); + +$stmt = $pdo->prepare( + 'UPDATE cja_tasks SET status = ?, result = ?, finished_at = NOW() WHERE task_id = ?' +); +$stmt->execute([$status, $result, $id]); + +echo json_encode(['ok' => true, 'id' => $id, 'status' => $status]); diff --git a/api/cli/tasks-next.php b/api/cli/tasks-next.php new file mode 100644 index 0000000..7c1aacf --- /dev/null +++ b/api/cli/tasks-next.php @@ -0,0 +1,53 @@ + running) guarded by + * task_id, so two concurrent callers can never claim the same row. + */ + +require __DIR__ . '/../vendor/autoload.php'; +require __DIR__ . '/../config.php'; + +$pdo = new PDO( + sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', DB_HOST, DB_NAME), + DB_USER, + DB_PASS, + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC] +); + +// Find + claim in a loop: SELECT the next candidate, then a guarded UPDATE. +// If the UPDATE affects 0 rows (someone else took it), try the next candidate. +for ($attempt = 0; $attempt < 20; $attempt++) { + $row = $pdo->query( + "SELECT task_id FROM cja_tasks WHERE status = 'queued' + ORDER BY sort_order ASC, task_id ASC LIMIT 1" + )->fetch(); + + if (!$row) { + echo "{}"; + exit; + } + + $id = (int) $row['task_id']; + $upd = $pdo->prepare( + "UPDATE cja_tasks SET status = 'running', started_at = NOW() + WHERE task_id = ? AND status = 'queued'" + ); + $upd->execute([$id]); + + if ($upd->rowCount() === 1) { + $task = $pdo->prepare('SELECT task_id, title, target_page, prompt, draft, assets FROM cja_tasks WHERE task_id = ?'); + $task->execute([$id]); + echo json_encode($task->fetch(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + exit; + } + // lost the race — loop and grab the next one +} + +echo "{}"; diff --git a/api/db/migrations/011_create_cja_tasks.sql b/api/db/migrations/011_create_cja_tasks.sql new file mode 100644 index 0000000..030f0e9 --- /dev/null +++ b/api/db/migrations/011_create_cja_tasks.sql @@ -0,0 +1,35 @@ +-- Web Designer task queue — design requests the agent drains one at a time. +-- +-- The admin submits a brief (target page, prompt, draft, assets) which lands +-- here as `queued`. The console runner claims the oldest queued row, runs the +-- design agent, builds, and auto-publishes (git commit), then marks it +-- `published` (or `failed`). The queue is FIFO by (sort_order, task_id). +-- +-- `assets` is JSON: { "images": [{ "url": "/media/x.webp", "alt": "" }], +-- "videos": ["https://instagram.com/reel/..."] } +-- `result` is JSON, written on completion: { commit, summary, live, error }. + +CREATE TABLE IF NOT EXISTS `cja_tasks` ( + `task_id` int(10) unsigned NOT NULL AUTO_INCREMENT, + + `title` varchar(200) NOT NULL DEFAULT '', + -- "/about", "/services", or "new:" to create a brand-new page + `target_page` varchar(200) NOT NULL DEFAULT '', + + `prompt` text DEFAULT NULL, -- the request / special instructions + `draft` mediumtext DEFAULT NULL, -- optional draft content + `assets` longtext DEFAULT NULL CHECK (json_valid(`assets`)), + + `status` enum('queued','running','published','failed','canceled') + NOT NULL DEFAULT 'queued', + `result` longtext DEFAULT NULL CHECK (`result` IS NULL OR json_valid(`result`)), + + `sort_order` int(11) NOT NULL DEFAULT 0, + + `created_at` datetime NOT NULL DEFAULT current_timestamp(), + `started_at` datetime DEFAULT NULL, + `finished_at` datetime DEFAULT NULL, + + PRIMARY KEY (`task_id`), + KEY `idx_cja_tasks_queue` (`status`, `sort_order`, `task_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/app/src/components/Footer.astro b/app/src/components/Footer.astro index 63925f9..1c2776c 100644 --- a/app/src/components/Footer.astro +++ b/app/src/components/Footer.astro @@ -70,6 +70,7 @@ const { flush = false } = Astro.props;
  • Resume
  • Contact
  • Changelog
  • +
  • Queue Test
  • diff --git a/app/src/components/Header.astro b/app/src/components/Header.astro index a56c7ff..ed09ace 100644 --- a/app/src/components/Header.astro +++ b/app/src/components/Header.astro @@ -10,6 +10,7 @@ const nav = [ { to: "/about", label: "About" }, { to: "/resume", label: "Resume" }, { to: "/contact", label: "Contact Me" }, + { to: "/zzz-queue-test", label: "Queue Test" }, ]; const current = Astro.url.pathname.replace(/\/$/, "") || "/"; diff --git a/app/src/pages/zzz-queue-test.astro b/app/src/pages/zzz-queue-test.astro new file mode 100644 index 0000000..cd8ca4f --- /dev/null +++ b/app/src/pages/zzz-queue-test.astro @@ -0,0 +1,22 @@ +--- +import BaseLayout from "../layouts/BaseLayout.astro"; +import { SITE } from "../lib/blog-data.js"; +--- + + +
    +

    Internal

    +

    + Queue Test +

    +

    + This is a temporary placeholder page used to validate the publishing queue. It will be removed once testing is complete. +

    +
    +