quc v0 (3/4): the page and the API

page.js: start screen, live chair view (read-only diffs and commentary), the safeword, the box, a replay scrubber for anyone with the link, the aftermath card and patch download. worker.js: the API with a visitor cookie, same-origin POSTs and daily caps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVg99kQMY2Tumhobuyg7Fm
This commit is contained in:
teahubxyzandClaude Opus 5.5 committed 2026-10-22 18:14:00 +00:00
1 parent 2ad896b9b5
commit e7c0ec1188
2 files changed
+332 -101

No files matched your search

+233
View File
@@ -0,0 +1,233 @@
// The one page: start screen at /, the chair (live run, or the replay with its scrubber) at /run/:id.
// The browser code lives in real functions below and is inlined with toString(), so there is nothing to escape and no build.
import { validate } from "./validate.js";
export const CHAIR = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" role="img" aria-label="A chair in the corner of the room">
<path d="M0 118h120M118 0v120" stroke="currentColor" stroke-width="2" fill="none" opacity=".35"/>
<g fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round">
<path d="M44 20 40 66M44 20h30l6 46M40 66h46M44 66l-4 44M82 66l4 44M66 66l-2 44"/></g></svg>`;
const CSS = `
:root{--bg:#f7f5f0;--fg:#1b1a17;--mute:#6d6b64;--line:#e2dfd6;--card:#fffdf8;--accent:#7a3cff;--accent-fg:#fff;--add:#e3f6e8;--add-fg:#14532d;--del:#fbe7e7;--del-fg:#7f1d1d;--warn:#b42318;--serif:ui-serif,Georgia,"Times New Roman",serif;--sans:system-ui,-apple-system,"Segoe UI",sans-serif;--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
@media (prefers-color-scheme:dark){:root{--bg:#141312;--fg:#ecebe6;--mute:#9a988f;--line:#2b2a27;--card:#1b1a18;--accent:#a98bff;--accent-fg:#141312;--add:#12301d;--add-fg:#b6f0c6;--del:#3a1616;--del-fg:#ffc2c2;--warn:#ff8a80}}
*{box-sizing:border-box}
body{margin:0;min-height:100vh;min-height:100dvh;background:var(--bg);color:var(--fg);font:16px/1.55 var(--sans)}
a{color:var(--accent)}
header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;border-bottom:1px solid var(--line);max-width:1200px;margin:0 auto}
header a.brand{font:600 22px var(--serif);color:var(--fg);text-decoration:none}
main{max-width:1200px;margin:0 auto;padding:24px 16px 180px}
h1{font:600 clamp(30px,6vw,52px)/1.08 var(--serif);margin:0 0 12px;letter-spacing:-.01em}
h2{font:600 20px var(--serif);margin:0 0 8px}
p{margin:0 0 14px}.mute{color:var(--mute)}
[hidden]{display:none!important}
form{max-width:560px;margin-top:20px}
label{display:block;font-weight:600;margin-bottom:4px}
input,button,textarea{font:inherit;color:var(--fg)}
input[type=text],input[type=url]{width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card)}
input[aria-invalid=true]{border-color:var(--warn)}
button{padding:10px 16px;border-radius:8px;border:1px solid var(--line);background:var(--card);cursor:pointer;font-weight:600}
button.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-fg)}
button.safeword{background:#c62828;border-color:#c62828;color:#fff;font-size:17px;padding:12px 20px}
button:disabled{opacity:.5;cursor:default}
.field{margin-bottom:16px}.err{color:var(--warn);font-size:14px;margin:4px 0 0;min-height:1em}
.notice{padding:10px 12px;border-left:3px solid var(--accent);background:var(--card);font-size:14px}
.rules{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin:28px 0;max-width:900px}
.rules div{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:14px}
.rules b{display:block;font-family:var(--serif);font-size:18px;margin-bottom:4px}
.bar{display:flex;flex-wrap:wrap;align-items:center;gap:10px 16px;margin-bottom:16px}
.status{font-size:14px;padding:4px 10px;border-radius:99px;background:var(--card);border:1px solid var(--line)}
.status[data-s=running]::before{content:"";display:inline-block;width:8px;height:8px;border-radius:50%;background:#2a9d4a;margin-right:6px;animation:pulse 1.2s infinite}
@keyframes pulse{50%{opacity:.25}}
.grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.5fr);gap:16px}
@media (max-width:820px){.grid{grid-template-columns:minmax(0,1fr)}}
.panel{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px;min-width:0}
.feed{display:flex;flex-direction:column;gap:10px;max-height:62vh;overflow:auto}
.say{border-left:3px solid var(--accent);padding:2px 0 2px 10px;font-family:var(--serif);font-size:17px}
.sys{font-size:13px;color:var(--mute)}
.note{align-self:flex-end;background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:6px 10px;font-size:14px;max-width:85%}
.unlock{font-size:13px;font-weight:600;color:var(--accent)}
.files{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
.files button{font:12px var(--mono);padding:4px 8px;font-weight:400}
.files button[aria-pressed=true]{border-color:var(--accent);color:var(--accent)}
pre.diff{margin:0;font:12.5px/1.45 var(--mono);overflow:auto;max-height:62vh;white-space:pre}
pre.diff span{display:block;padding:0 8px}
pre.diff .a{background:var(--add);color:var(--add-fg)}pre.diff .d{background:var(--del);color:var(--del-fg)}pre.diff .h{color:var(--mute)}
.commit{font:12px var(--mono);color:var(--mute);margin:0 0 8px}
.saybox{display:flex;gap:8px;margin-top:12px}.saybox input{flex:1}
.scrub{display:flex;align-items:center;gap:10px;margin:0 0 16px}.scrub input{flex:1;accent-color:var(--accent)}
.after{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin:0 0 16px}
.after div{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px}
.after b{display:block;font:600 24px var(--serif)}
.chair{position:fixed;right:0;bottom:0;width:clamp(84px,18vw,150px);color:var(--fg);pointer-events:none;opacity:.85}
`;
// ---------- browser code ----------
function client(validate) {
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
const api = async (path, opts = {}) => {
const r = await fetch(path, { ...opts, headers: { "content-type": "application/json", ...(opts.headers || {}) } });
const j = await r.json().catch(() => ({}));
if (!r.ok) throw Object.assign(new Error(j.error || r.statusText), { status: r.status, body: j });
return j;
};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
fetch("/chair.svg").then((r) => r.text()).then((s) => { const d = document.createElement("div"); d.className = "chair"; d.innerHTML = s; d.firstElementChild.setAttribute("aria-hidden", "true"); document.body.append(d); }).catch(() => {});
const m = location.pathname.match(/^\/run\/([\w-]+)$/);
if (!m) return startScreen();
return chair(m[1]);
function startScreen() {
$("start").hidden = false;
$("form").addEventListener("submit", async (ev) => {
ev.preventDefault();
const repo = $("repo").value.trim(), task = $("task").value.trim(), errs = validate(repo, task);
for (const k of ["repo", "task"]) { $(k + "-err").textContent = errs[k] || ""; $(k).setAttribute("aria-invalid", !!errs[k]); }
if (Object.keys(errs).length) return $(errs.repo ? "repo" : "task").focus();
const b = $("go"); b.disabled = true; b.textContent = "Taking your seat…";
try {
const j = await api("/api/runs", { method: "POST", body: JSON.stringify({ repo, task }) });
location.assign("/run/" + j.id);
} catch (e) {
if (e.body?.id) return location.assign("/run/" + e.body.id);
$("task-err").textContent = e.message; b.disabled = false; b.textContent = "Hand it over";
}
});
}
function chair(id) {
$("run").hidden = false;
document.title = "quc: run " + id;
let run = null, owner = false, events = [], at = 0, live = false, pick = null;
const render = () => {
if (!run) return;
const shown = events.slice(0, at);
$("run-task").textContent = run.task;
$("run-repo").textContent = run.repo; $("run-repo").href = run.repo;
const st = run.status === "ended" ? { finished: "Finished", safeworded: "Safeworded", unlocked: "You took the wheel" }[run.outcome] : run.status === "queued" ? "Taking over…" : "Working on your repo";
$("status").textContent = st; $("status").dataset.s = run.status;
$("safeword").hidden = !(owner && run.status !== "ended");
$("saybox").hidden = !(owner && run.status !== "ended");
$("level").textContent = owner && run.status !== "ended" ? ["Caged. You can only watch.", "Notch 1: you may leave notes.", "Notch 2: name a file path to veto it.", ""][run.level || 0] : "";
$("feed").innerHTML = shown.filter((e) => e.kind !== "diff" && e.kind !== "commit").map((e) =>
e.kind === "commentary" ? `<div class="say">${esc(e.data.text)}</div>`
: e.kind === "note" ? `<div class="note">You: ${esc(e.data.text)}</div>`
: e.kind === "unlock" ? `<div class="unlock">Unlocked notch ${e.data.level}</div>`
: `<div class="sys">${esc(e.data.text)}</div>`).join("") || `<div class="sys">Nothing yet. Sit.</div>`;
$("feed").scrollTop = $("feed").scrollHeight;
const diffs = shown.filter((e) => e.kind === "diff");
const latest = {};
for (const d of diffs) latest[d.data.path] = d;
const paths = Object.keys(latest);
const focus = pick && latest[pick] ? latest[pick] : diffs[diffs.length - 1];
$("files").innerHTML = paths.map((p) => `<button type="button" data-p="${esc(p)}" aria-pressed="${focus && focus.data.path === p}">${esc(p)}${latest[p].data.reverted ? " (vetoed)" : ""}</button>`).join("");
const commit = focus && shown.find((e) => e.kind === "commit" && e.seq === focus.seq + 1);
$("commit").textContent = commit ? "commit: " + commit.data.message : focus?.data.reverted ? "reverted on your veto" : "";
$("diff").innerHTML = focus ? focus.data.patch.split("\n").map((l) => `<span class="${l.startsWith("+++") || l.startsWith("---") || l.startsWith("@@") ? "h" : l[0] === "+" ? "a" : l[0] === "-" ? "d" : ""}">${esc(l) || " "}</span>`).join("")
: `<span class="h">No changes yet. It hasn't touched anything. Yet.</span>`;
const ended = run.status === "ended";
$("scrub").hidden = !(ended || !owner) || !events.length;
$("scrub-range").max = events.length; $("scrub-range").value = at;
$("scrub-at").textContent = `${at} / ${events.length}`;
$("after").hidden = !ended;
if (ended) {
$("a-files").textContent = run.stats.files; $("a-add").textContent = "+" + run.stats.added; $("a-del").textContent = "-" + run.stats.removed;
$("a-patch").hidden = run.outcome === "safeworded" || !run.stats.files;
$("a-patch").href = `/api/runs/${id}/patch`;
}
};
$("files").addEventListener("click", (e) => { const p = e.target.closest("button")?.dataset.p; if (p) { pick = p; render(); } });
$("scrub-range").addEventListener("input", (e) => { at = Number(e.target.value); live = false; render(); });
$("share").addEventListener("click", async () => { try { await navigator.clipboard.writeText(location.href); $("share").textContent = "Link copied"; } catch { prompt("Copy the replay link", location.href); } });
$("safeword").addEventListener("click", async () => {
$("safeword").disabled = true;
try { await api(`/api/runs/${id}/safeword`, { method: "POST", body: "{}" }); } finally { await refresh(); }
});
$("saybox").addEventListener("submit", async (e) => {
e.preventDefault();
const text = $("say").value.trim(); if (!text) return;
$("say").value = "";
try { await api(`/api/runs/${id}/say`, { method: "POST", body: JSON.stringify({ text }) }); } catch (err) { $("say-err").textContent = err.message; }
await refresh();
});
async function refresh() {
const j = await api(`/api/runs/${id}?after=${events.length}`);
const follow = at === events.length;
run = j.run; owner = j.owner;
events = events.concat(j.events.filter((e) => e.seq > events.length));
if (follow || live) at = events.length;
render();
}
(async () => {
try { await refresh(); } catch (e) { $("run-task").textContent = e.status === 404 ? "No such run." : e.message; return; }
live = true;
if (run.status !== "ended") {
(async () => { while (run.status !== "ended") { await sleep(2000); await refresh().catch(() => {}); } })();
while (owner && run.status !== "ended") {
try { await api(`/api/runs/${id}/step`, { method: "POST", body: "{}" }); } catch { await sleep(2000); }
await refresh().catch(() => {});
await sleep(300);
}
} else if (events.length) {
at = 0; render();
for (let i = 1; i <= events.length && !document.hidden; i++) { if (at !== i - 1) break; at = i; render(); await sleep(350); }
}
})();
}
}
export const PAGE = `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>quc</title>
<meta name="description" content="Hand quc your repo and one line of task. It does the work. You sit in the chair and watch.">
<style>${CSS}</style></head><body>
<header><a class="brand" href="/">quc</a><span class="mute" style="font-size:14px">you sit, it codes</span></header>
<main>
<section id="start" hidden>
<h1>Hand over your repo.<br>Take a seat.</h1>
<p class="mute" style="max-width:620px">quc takes a public GitHub repo and one line of task, then does whatever it wants to your code while you watch from the corner. No approve buttons. No edits. You get a chair, a safeword, and a replay.</p>
<form id="form" novalidate>
<div class="field"><label for="repo">Public GitHub repo</label>
<input id="repo" type="url" inputmode="url" autocomplete="off" placeholder="https://github.com/owner/name" aria-describedby="repo-err">
<p class="err" id="repo-err" role="alert"></p></div>
<div class="field"><label for="task">One line of task</label>
<input id="task" type="text" autocomplete="off" maxlength="200" placeholder="Make the README less embarrassing" aria-describedby="task-err">
<p class="err" id="task-err" role="alert"></p></div>
<p class="notice">Replays are public: anyone with the link can watch. quc only reads your repo. It never runs your code and never pushes anything; you leave with a patch, if you want it.</p>
<button class="primary" id="go" type="submit">Hand it over</button>
</form>
<div class="rules">
<div><b>The cage</b>It works on its own copy, fully unsupervised. Your repo is never touched.</div>
<div><b>The chair</b>You watch every change land, live and read-only, while it talks down to you.</div>
<div><b>The safeword</b>One button. Everything stops and its work is thrown away.</div>
<div><b>The magic words</b>Say the right thing and it lets you speak, veto a file, or take the wheel.</div>
</div>
</section>
<section id="run" hidden>
<div class="bar"><span class="status" id="status">…</span><button class="safeword" id="safeword" hidden>Safeword</button><button id="share" type="button">Copy replay link</button><span class="mute" id="level" style="font-size:14px"></span></div>
<h1 id="run-task" style="font-size:clamp(24px,4.5vw,38px)">Loading…</h1>
<p class="mute" style="font-size:14px">on <a id="run-repo" rel="noopener" target="_blank"></a></p>
<div class="after" id="after" hidden>
<div><span class="mute">Files changed</span><b id="a-files">0</b></div><div><span class="mute">Lines added</span><b id="a-add">+0</b></div><div><span class="mute">Lines removed</span><b id="a-del">-0</b></div>
<div><span class="mute">Nothing was run or pushed.</span><a id="a-patch" download="quc.patch" style="display:block;margin-top:6px;font-weight:600" hidden>Download the patch</a></div>
</div>
<div class="scrub" id="scrub" hidden><label for="scrub-range" class="mute" style="font-size:14px">Replay</label><input type="range" id="scrub-range" min="0" step="1"><span class="mute" id="scrub-at" style="font:13px var(--mono)"></span></div>
<div class="grid">
<div class="panel"><h2>From the corner</h2><div class="feed" id="feed" aria-live="polite"></div>
<form class="saybox" id="saybox" hidden><input id="say" type="text" maxlength="300" autocomplete="off" placeholder="Say something…" aria-label="Say something"><button type="submit">Say</button></form>
<p class="err" id="say-err"></p></div>
<div class="panel"><h2>What it's doing to your code</h2><div class="files" id="files"></div><p class="commit" id="commit"></p><pre class="diff" id="diff"></pre></div>
</div>
</section>
</main>
<script type="module">
${validate}
(${client})(validate);
</script></body></html>`;
+99 -101
View File
@@ -1,114 +1,112 @@
// quc: app shell + API in one Worker (no build step; the same module runs on Cloudflare and under dev.mjs).
// GET / the shell (start form) GET /run/:id the shell (chair view) GET /chair.svg the chair-in-the-corner motif
// GET /api/health {ok, rev} anything else 404
// Fonts: system stacks on purpose (no webfont files, no third-party requests). shortcut: swap in self-hosted fonts when the brand picks them.
// quc: page + API in one Worker (no build step; the same module runs on Cloudflare and under dev.mjs).
// GET /, /run/:id the page (start screen, or the chair / replay)
// GET /chair.svg the chair-in-the-corner motif
// GET /api/health {ok, rev, live} live: false when no model key is bound (offline demo answers)
// POST /api/runs {repo, task} -> {id} sets the visitor cookie
// GET /api/runs/:id?after=N {run, events, owner} anyone with the link
// POST /api/runs/:id/step advance one step owner only
// POST /api/runs/:id/safeword stop, throw the work away owner only
// POST /api/runs/:id/say {text}: talk from the chair owner only
// GET /api/runs/:id/patch the run's changes as a unified diff
// Bindings: KV (a KV namespace), VENICE_API_KEY (secret; without it the agent answers offline), REV, CODER_MODEL, PERSONA_MODEL,
// RUNS_PER_DAY (per visitor, default 5), RUNS_PER_DAY_ALL (default 300).
import { PAGE, CHAIR } from "./page.js";
import { createRun, getRun, getEvents, publicRun, memoryStore } from "./runs.js";
import { step, safeword, say, patch } from "./engine.js";
export const CHAIR = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" role="img" aria-label="A chair in the corner of the room">
<path d="M0 118h120M118 0v120" stroke="currentColor" stroke-width="2" fill="none" opacity=".35"/>
<g fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round">
<path d="M44 20 40 66M44 20h30l6 46M40 66h46M44 66l-4 44M82 66l4 44M66 66l-2 44"/></g></svg>`;
export { validate } from "./validate.js";
export { PAGE, CHAIR };
// Inlined into the page via toString(), so the browser and the tests run the same code. Returns {repo?, task?} errors; {} is valid.
export function validate(repo, task) {
const e = {};
if (!/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+?(\.git)?\/?$/.test(repo)) e.repo = "Use a public GitHub repo URL, like https://github.com/owner/name.";
if (task.length < 10) e.task = "Say a bit more: at least 10 characters.";
else if (task.length > 200) e.task = "Keep it to one line: 200 characters at most.";
return e;
const COOKIE = "quc_v";
const json = (body, status = 200, headers = {}) => Response.json(body, { status, headers: { "cache-control": "no-store", ...headers } });
const fallback = memoryStore(); // dev and tests only: a deployed Worker always has KV bound
function visitorOf(req) {
const m = (req.headers.get("cookie") || "").match(new RegExp(`(?:^|;\\s*)${COOKIE}=([A-Za-z0-9_-]{16,64})`));
return m ? m[1] : null;
}
const NOTICE = "Replays are public. Anyone with the link to a run can watch it, so don't point this at anything you wouldn't show a stranger.";
export const PAGE = `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>quc</title>
<meta name="description" content="Give quc a public repo and one line of task. It sits in the corner and does the work.">
<style>
:root{--bg:#faf9f6;--fg:#1d1d1b;--mute:#6b6a65;--line:#e3e1da;--accent:#2f6fdf;--err:#b3261e;--serif:ui-serif,Georgia,"Times New Roman",serif;--sans:system-ui,-apple-system,"Segoe UI",sans-serif}
@media (prefers-color-scheme:dark){:root{--bg:#161614;--fg:#ecebe6;--mute:#9b9a94;--line:#2c2b28;--accent:#7aa5ff;--err:#ff8a80}}
*{box-sizing:border-box}
body{margin:0;min-height:100vh;min-height:100dvh;display:flex;flex-direction:column;background:var(--bg);color:var(--fg);font:16px/1.5 var(--sans)}
header,footer{padding:16px;max-width:960px;width:100%;margin:0 auto}
header{border-bottom:1px solid var(--line);font:600 20px var(--serif)}
main{flex:1;max-width:960px;width:100%;margin:0 auto;padding:32px 16px 160px}
h1{font:600 clamp(28px,6vw,48px)/1.1 var(--serif);margin:0 0 12px}
p{margin:0 0 16px;color:var(--mute)}
[hidden]{display:none}
form{max-width:560px}
label{display:block;font-weight:600;margin-bottom:4px}
input,button{font:inherit;width:100%;padding:10px 12px;border:1px solid var(--line);border-radius:6px;background:transparent;color:var(--fg)}
input[aria-invalid=true]{border-color:var(--err)}
button{width:auto;background:var(--accent);border-color:var(--accent);color:var(--bg);font-weight:600;cursor:pointer}
.field{margin-bottom:16px}.field p{margin:4px 0 0}
.err{color:var(--err);font-size:14px}
.notice{max-width:560px;padding:10px 12px;border-left:3px solid var(--accent);background:var(--line);color:var(--fg);font-size:14px}
dt{font-weight:600}dd{margin:0 0 12px;overflow-wrap:anywhere}
#health::before{content:"";display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:8px;background:var(--mute)}
#health[data-ok=true]::before{background:#2a9d4a}#health[data-ok=false]::before{background:#d33}
.chair{position:fixed;right:0;bottom:0;width:clamp(96px,22vw,160px);color:var(--fg);pointer-events:none}
</style></head><body>
<header>quc</header>
<main>
<section id="start">
<h1>Leave it with the chair.</h1>
<p>Point quc at a public GitHub repo, say in one line what you want done, and go do something else. It works in the corner, quietly, and you can watch the replay.</p>
<form id="form" novalidate>
<div class="field"><label for="repo">Public GitHub repo</label>
<input id="repo" name="repo" type="url" inputmode="url" autocomplete="off" placeholder="https://github.com/owner/name" aria-describedby="repo-err">
<p class="err" id="repo-err" role="alert"></p></div>
<div class="field"><label for="task">One-line task</label>
<input id="task" name="task" type="text" autocomplete="off" maxlength="400" placeholder="Fix the flaky date test in utils" aria-describedby="task-err">
<p class="err" id="task-err" role="alert"></p></div>
<p class="notice" role="note">${NOTICE}</p>
<button type="submit">Start a run</button>
</form>
</section>
<section id="run" hidden>
<h1>Run <span id="run-id"></span></h1>
<p>The chair is in the corner. Nothing has been done yet.</p>
<dl><dt>Repo</dt><dd id="run-repo"></dd><dt>Task</dt><dd id="run-task"></dd></dl>
<p class="notice" role="note">${NOTICE}</p>
</section>
</main>
<footer><span id="health">checking…</span></footer>
<img class="chair" src="/chair.svg" alt="">
<script>
// the SVG uses currentColor, which an <img> cannot inherit: inline it instead
fetch("/chair.svg").then((r) => r.text()).then((s) => { const i = document.querySelector(".chair"), d = document.createElement("div"); d.className = "chair"; d.innerHTML = s; d.firstElementChild.setAttribute("aria-hidden", "true"); i.replaceWith(d); }).catch(() => {});
const h = document.getElementById("health");
fetch("/api/health").then((r) => r.json()).then((j) => { h.dataset.ok = !!j.ok; h.textContent = j.ok ? "API healthy" : "API unhealthy"; })
.catch(() => { h.dataset.ok = false; h.textContent = "API unreachable"; });
${validate}
const $ = (id) => document.getElementById(id);
const show = (repo, task, errs) => ["repo", "task"].forEach((k) => { $(k + "-err").textContent = errs[k] || ""; $(k).setAttribute("aria-invalid", !!errs[k]); });
// shortcut: no run store yet, so the run lives in the link (id + repo + task in the URL); move it server-side when runs persist.
$("form").addEventListener("submit", (ev) => {
ev.preventDefault();
const repo = $("repo").value.trim(), task = $("task").value.trim(), errs = validate(repo, task);
show(repo, task, errs);
if (Object.keys(errs).length) return $(errs.repo ? "repo" : "task").focus();
location.assign("/run/" + crypto.randomUUID().slice(0, 8) + "?" + new URLSearchParams({ repo, task }));
});
const m = location.pathname.match(/^\\/run\\/([\\w-]+)$/), q = new URLSearchParams(location.search);
if (m) {
const ok = !Object.keys(validate(q.get("repo") || "", q.get("task") || "")).length;
$("start").hidden = true; $("run").hidden = false;
$("run-id").textContent = m[1];
$("run-repo").textContent = ok ? q.get("repo") : "(unknown)"; $("run-task").textContent = ok ? q.get("task") : "(unknown)";
document.title = "quc: run " + m[1];
async function underCap(store, visitor, env) {
const day = new Date().toISOString().slice(0, 10);
const mine = `cap:${day}:${visitor}`, all = `cap:${day}:*`;
const [a, b] = [Number(await store.get(mine)) || 0, Number(await store.get(all)) || 0];
if (a >= Number(env.RUNS_PER_DAY || 5) || b >= Number(env.RUNS_PER_DAY_ALL || 300)) return false;
await store.put(mine, String(a + 1), { expirationTtl: 172800 });
await store.put(all, String(b + 1), { expirationTtl: 172800 });
return true;
}
</script></body></html>`;
const headers = (type, extra = {}) => ({ "content-type": type, ...extra });
async function api(req, env, url) {
const store = env.KV || fallback;
const path = url.pathname;
if (path === "/api/health") return json({ ok: true, rev: env.REV || "", live: !!env.VENICE_API_KEY });
if (req.method === "POST") {
const origin = req.headers.get("origin");
if (origin && origin !== url.origin) return json({ error: "cross-site request" }, 403);
}
let visitor = visitorOf(req);
const setCookie = {};
if (!visitor) {
visitor = crypto.randomUUID().replace(/-/g, "");
setCookie["set-cookie"] = `${COOKIE}=${visitor}; Path=/; Max-Age=31536000; HttpOnly; SameSite=Lax${url.protocol === "https:" ? "; Secure" : ""}`;
}
const body = req.method === "POST" ? await req.json().catch(() => ({})) : {};
if (path === "/api/runs" && req.method === "POST") {
try {
const repo = String(body.repo || "").trim(), task = String(body.task || "").trim();
const active = await store.get(`active:${visitor}`);
if (!active && !(await underCap(store, visitor, env))) return json({ error: "That's enough for today. The chair needs a rest; come back tomorrow." }, 429, setCookie);
const run = await createRun(store, { repo, task, visitor });
return json({ id: run.id }, 201, setCookie);
} catch (e) {
if (e.code === "active") return json({ error: e.message, id: e.id }, 409, setCookie);
if (e.code === "invalid") return json({ error: Object.values(e.errors)[0], errors: e.errors }, 400, setCookie);
throw e;
}
}
const m = path.match(/^\/api\/runs\/([a-z0-9]{6,16})(?:\/(step|safeword|say|patch))?$/);
if (!m) return json({ error: "not found" }, 404);
const [, id, action] = m;
const run = await getRun(store, id);
if (!run) return json({ error: "no such run" }, 404, setCookie);
const owner = run.visitor === visitor;
if (!action && req.method === "GET") {
const after = Number(url.searchParams.get("after")) || 0;
const events = (await getEvents(store, id)).filter((e) => e.seq > after);
return json({ run: publicRun(run), events, owner }, 200, setCookie);
}
if (action === "patch" && req.method === "GET") {
const text = await patch(store, id);
return new Response(text, { headers: { "content-type": "text/x-diff; charset=utf-8", "content-disposition": `attachment; filename="quc-${id}.patch"` } });
}
if (req.method !== "POST") return json({ error: "method not allowed" }, 405);
if (!owner) return json({ error: "Only the one in the chair can do that." }, 403, setCookie);
try {
if (action === "step") return json({ run: publicRun(await step(store, id, env)) });
if (action === "safeword") return json({ run: publicRun(await safeword(store, id)) });
if (action === "say") return json({ reply: await say(store, id, body.text) });
} catch (e) {
if (e.code) return json({ error: e.message }, 400);
throw e;
}
return json({ error: "not found" }, 404);
}
export default {
async fetch(req, env = {}) {
const { pathname } = new URL(req.url);
const url = new URL(req.url);
try {
if (url.pathname.startsWith("/api/")) return await api(req, env, url);
} catch (e) {
return json({ error: "Something broke in the corner: " + String(e.message || e).slice(0, 200) }, 500);
}
if (req.method !== "GET" && req.method !== "HEAD") return new Response("method not allowed", { status: 405 });
if (pathname === "/api/health") return Response.json({ ok: true, rev: env.REV || "" }, { headers: { "cache-control": "no-store" } });
if (pathname === "/chair.svg") return new Response(CHAIR, { headers: headers("image/svg+xml", { "cache-control": "public, max-age=3600" }) });
if (pathname === "/" || pathname === "/index.html" || /^\/run\/[\w-]+$/.test(pathname)) return new Response(PAGE, { headers: headers("text/html; charset=utf-8") });
if (url.pathname === "/chair.svg") return new Response(CHAIR, { headers: { "content-type": "image/svg+xml", "cache-control": "public, max-age=3600" } });
if (url.pathname === "/" || url.pathname === "/index.html" || /^\/run\/[\w-]+$/.test(url.pathname))
return new Response(PAGE, { headers: { "content-type": "text/html; charset=utf-8", "x-frame-options": "DENY", "referrer-policy": "same-origin" } });
return new Response("not found", { status: 404 });
},
};