/* repospawner UI — request table plus the submission form. No build step, no CDNs; everything the page needs ships in the binary. */ (function () { "use strict"; var REFRESH_MS = 10000; var FIELDS = ["name", "description", "status_checks"]; var BADGES = { "opening-pr": "badge-run", "pr-open": "badge-run", "merged": "badge-run", "enabling-ci": "badge-run", "ready": "badge-ok", "closed": "badge-warn", "failed": "badge-err" }; var el = function (id) { return document.getElementById(id); }; var toastTimer = null; function toast(msg, isErr) { var t = el("toast"); t.textContent = msg; t.className = "toast" + (isErr ? " err" : ""); if (toastTimer) { clearTimeout(toastTimer); } toastTimer = setTimeout(function () { t.className = "toast hidden"; }, 6000); } function clearFieldErrors() { FIELDS.forEach(function (f) { var e = el("e-" + f); e.textContent = ""; e.className = "field-error hidden"; }); } function showFieldErrors(fields) { Object.keys(fields || {}).forEach(function (f) { var e = el("e-" + f); if (!e) { return; } e.textContent = fields[f]; e.className = "field-error"; }); } function age(iso) { var secs = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000); if (secs < 60) { return Math.floor(secs) + "s"; } if (secs < 3600) { return Math.floor(secs / 60) + "m"; } if (secs < 86400) { return Math.floor(secs / 3600) + "h"; } return Math.floor(secs / 86400) + "d"; } function cell(row, text, cls) { var td = document.createElement("td"); if (cls) { td.className = cls; } if (text !== undefined && text !== null) { td.textContent = text; } row.appendChild(td); return td; } function render(requests) { var body = el("rows"); body.textContent = ""; el("empty").className = requests.length ? "muted hidden" : "muted"; requests.forEach(function (r) { var tr = document.createElement("tr"); var nameCell = cell(tr, null); var name = document.createElement("div"); name.className = "name"; name.textContent = r.name; nameCell.appendChild(name); if (r.description) { var desc = document.createElement("div"); desc.className = "desc"; desc.textContent = r.description; nameCell.appendChild(desc); } var stateCell = cell(tr, null); var badge = document.createElement("span"); badge.className = "badge " + (BADGES[r.state] || ""); badge.textContent = r.state; stateCell.appendChild(badge); if (r.error) { var err = document.createElement("div"); err.className = "desc"; err.textContent = r.error; stateCell.appendChild(err); } var prCell = cell(tr, null); if (r.pr_url) { var a = document.createElement("a"); a.href = r.pr_url; a.target = "_blank"; a.rel = "noreferrer noopener"; a.textContent = "#" + r.pr_number; prCell.appendChild(a); } else { prCell.textContent = "—"; prCell.className = "muted"; } if (!r.woodpecker) { cell(tr, "not requested", "muted"); } else { cell(tr, r.ci_enabled ? "enabled" : "pending", r.ci_enabled ? "" : "muted"); } cell(tr, age(r.created), "muted"); body.appendChild(tr); }); el("refreshed").textContent = "updated " + new Date().toLocaleTimeString(); } function refresh() { return fetch("/api/requests", { headers: { Accept: "application/json" } }) .then(function (resp) { if (!resp.ok) { throw new Error("list failed: " + resp.status); } return resp.json(); }) .then(function (data) { render(data.requests || []); }) .catch(function (err) { toast(err.message, true); }); } function capabilities() { return fetch("/api/capabilities", { headers: { Accept: "application/json" } }) .then(function (resp) { return resp.ok ? resp.json() : { woodpecker: false }; }) .then(function (caps) { if (!caps.woodpecker) { el("f-woodpecker").checked = false; el("f-woodpecker").disabled = true; el("wp-unavailable").className = "help"; } }) .catch(function () { /* the checkbox stays enabled; the API still refuses */ }); } function submit(ev) { ev.preventDefault(); clearFieldErrors(); var btn = el("submit"); btn.disabled = true; var payload = { name: el("f-name").value.trim(), description: el("f-description").value.trim(), woodpecker: el("f-woodpecker").checked, status_checks: el("f-checks").value.split("\n").map(function (s) { return s.trim(); }) .filter(function (s) { return s.length > 0; }) }; fetch("/api/requests", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(payload) }) .then(function (resp) { return resp.json().catch(function () { return {}; }).then(function (data) { return { ok: resp.ok, status: resp.status, data: data }; }); }) .then(function (res) { if (res.ok) { toast("Request " + res.data.id + " accepted; the pull request URL appears here shortly."); el("f-name").value = ""; el("f-description").value = ""; return refresh(); } showFieldErrors(res.data.fields); toast(res.data.error || ("request rejected: " + res.status), true); return null; }) .catch(function (err) { toast(err.message, true); }) .then(function () { btn.disabled = false; }); } document.addEventListener("DOMContentLoaded", function () { el("new-form").addEventListener("submit", submit); capabilities(); refresh(); setInterval(refresh, REFRESH_MS); }); })();