card #222: update quc/src/worker.js
time/worker-run cd quc && cat >> test/worker.test.js <<'EOF' test("validate accepts a repo + task and rejects bad s

This commit is contained in:
time committed 2026-10-22 09:08:25 +00:00
1 parent 7a805ce5ae
commit 0422bc78cf
1 file changed
+69 -7
+69 -7
View File
@@ -1,5 +1,5 @@
// quc: app shell + API in one Worker (no build step; the same module runs on Cloudflare and under dev.mjs).
// GET / the shell GET /chair.svg the chair-in-the-corner motif
// 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.
@@ -8,13 +8,24 @@ export const CHAIR = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 1
<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>`;
// 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 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="quc">
<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;--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}}
: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}
@@ -22,13 +33,44 @@ 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><h1>Coming together.</h1><p>The app shell is up. Nothing to do here yet.</p><p id="health">checking…</p></main>
<footer></footer>
<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
@@ -36,6 +78,26 @@ fetch("/chair.svg").then((r) => r.text()).then((s) => { const i = document.query
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];
}
</script></body></html>`;
const headers = (type, extra = {}) => ({ "content-type": type, ...extra });
@@ -46,7 +108,7 @@ export default {
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") return new Response(PAGE, { headers: headers("text/html; charset=utf-8") });
if (pathname === "/" || pathname === "/index.html" || /^\/run\/[\w-]+$/.test(pathname)) return new Response(PAGE, { headers: headers("text/html; charset=utf-8") });
return new Response("not found", { status: 404 });
},
};