Menu
akurai-tasks
publicLatest change 2c9c32aa6fc1bd2551a09c6a1fc1e71b2dd98c79 - feat: complete and deploy AkurAI Tasks by Ólafur Búi Ólafsson
const states = ["Inbox", "Triage", "Discovery", "Ready", "In Progress", "Review", "Verification", "Release Ready", "Done"];
const app = {
actor: "",
leases: JSON.parse(sessionStorage.getItem("akuraiTaskLeases") || "{}"),
projects: [],
project: null,
board: null,
task: null,
view: "board",
};
const $ = (selector) => document.querySelector(selector);
const projectsEl = $("#projects");
const boardEl = $("#board");
const frontierEl = $("#frontier");
const loadingEl = $("#loading");
const emptyEl = $("#empty");
const projectDialog = $("#project-dialog");
const taskDialog = $("#task-dialog");
const detailDialog = $("#task-detail");
async function api(path, options = {}) {
const headers = { accept: "application/json", ...(options.headers || {}) };
if (options.body) {
headers["Content-Type"] = "application/json";
headers["Idempotency-Key"] = crypto.randomUUID();
const csrf = document.cookie.split("; ").find((part) => part.startsWith("tasks_csrf="))?.split("=")[1];
if (csrf) headers["X-CSRF-Token"] = csrf;
const lease = app.task?.id ? app.leases[app.task.id] : null;
if (lease) {
headers["X-Lease-Token"] = lease.token;
headers["X-Lease-Generation"] = String(lease.generation);
}
}
const response = await fetch(path, { ...options, headers, credentials: "same-origin" });
const payload = await response.json().catch(() => ({}));
if (response.status === 401) {
window.location.assign("/auth/login");
throw new Error("Redirecting to sign in");
}
if (!response.ok) throw new Error(payload.error || `Request failed with ${response.status}`);
return payload;
}
function toast(message, kind = "ok") {
const item = document.createElement("div");
item.className = `toast ${kind}`;
const text = document.createElement("span");
text.textContent = message;
const dismiss = document.createElement("button");
dismiss.type = "button";
dismiss.textContent = "Dismiss";
dismiss.setAttribute("aria-label", "Dismiss notification");
dismiss.addEventListener("click", () => item.remove());
item.append(text, dismiss);
$("#toasts").append(item);
setTimeout(() => item.remove(), 5200);
}
function setHealth(ok, label) {
const health = $("#health");
health.classList.toggle("ok", ok);
health.lastChild.textContent = label;
}
async function boot() {
try {
await api("/health");
setHealth(true, "Service ready");
} catch {
setHealth(false, "Service unavailable");
}
try {
const identity = await api("/api/v1/me");
app.actor = identity.principal;
$("#session-button").textContent = app.actor;
await loadProjects();
} catch (error) {
if (!String(error.message).includes("Redirecting")) toast(error.message, "error");
}
}
async function loadProjects(selectKey) {
try {
app.projects = await api("/api/projects");
renderProjects();
const key = selectKey || app.project?.key || app.projects[0]?.key;
if (key) await selectProject(key);
else showEmpty();
} catch (error) {
if (!String(error.message).includes("Redirecting")) toast(error.message, "error");
}
}
function renderProjects() {
projectsEl.replaceChildren();
$("#project-count").textContent = app.projects.length;
for (const project of app.projects) {
const button = document.createElement("button");
button.type = "button";
button.className = "project-button";
button.classList.toggle("active", app.project?.key === project.key);
button.dataset.key = project.key;
const mark = document.createElement("span");
mark.className = "project-mark";
mark.textContent = project.key.slice(0, 2);
const copy = document.createElement("span");
const name = document.createElement("strong");
name.textContent = project.name;
const meta = document.createElement("small");
meta.textContent = `${project.repos.length} ${project.repos.length === 1 ? "repository" : "repositories"}`;
copy.append(name, meta);
button.append(mark, copy);
button.addEventListener("click", () => selectProject(project.key));
projectsEl.append(button);
}
}
async function selectProject(key) {
app.project = app.projects.find((project) => project.key === key);
renderProjects();
$("#project-key").textContent = app.project.key;
$("#project-title").textContent = app.project.name;
$("#project-repos").textContent = app.project.repos.length ? app.project.repos.join(" · ") : "Project-wide tasks";
$("#new-task").disabled = false;
const select = $("#task-form select[name=repo]");
select.replaceChildren(new Option("Project-wide", ""));
for (const repo of app.project.repos) select.add(new Option(repo, repo));
emptyEl.hidden = true;
await loadBoard();
}
async function loadBoard() {
if (!app.project) return;
loadingEl.hidden = false;
boardEl.hidden = true;
frontierEl.hidden = true;
try {
if (app.view === "frontier") {
const items = await api(`/api/projects/${app.project.key}/frontier`);
renderFrontier(items);
} else {
app.board = await api(`/api/projects/${app.project.key}/board`);
renderBoard();
}
} catch (error) {
toast(error.message, "error");
} finally {
loadingEl.hidden = true;
}
}
function renderBoard() {
boardEl.replaceChildren();
for (const column of app.board.columns) {
const section = document.createElement("section");
section.className = "column";
const heading = document.createElement("header");
const title = document.createElement("h2");
title.textContent = column.state;
const count = document.createElement("span");
count.textContent = column.items.length;
heading.append(title, count);
section.append(heading);
const list = document.createElement("div");
list.className = "card-list";
if (!column.items.length) {
const empty = document.createElement("p");
empty.className = "column-empty";
empty.textContent = "No work";
list.append(empty);
}
for (const item of column.items) list.append(taskCard(item));
section.append(list);
boardEl.append(section);
}
boardEl.hidden = false;
}
function taskCard(item) {
const button = document.createElement("button");
button.type = "button";
button.className = `task-card priority-${item.priority}`;
const top = document.createElement("span");
top.className = "card-topline";
const id = document.createElement("span");
id.textContent = item.id;
const priority = document.createElement("span");
priority.textContent = item.priority;
top.append(id, priority);
const title = document.createElement("strong");
title.textContent = item.title;
const repo = document.createElement("small");
repo.textContent = item.repo || "Project-wide";
button.append(top, title, repo);
if (item.blocked) {
const blocked = document.createElement("span");
blocked.className = "blocked";
blocked.textContent = "Blocked";
button.append(blocked);
}
button.addEventListener("click", () => openTask(item));
return button;
}
function renderFrontier(items) {
frontierEl.replaceChildren();
const header = document.createElement("div");
header.className = "frontier-heading";
header.innerHTML = `<p class="overline">Pull system</p><h2>Ready execution frontier</h2><p>Dependency-free, unblocked leaf work available to claim.</p>`;
frontierEl.append(header);
if (!items.length) {
const empty = document.createElement("p");
empty.className = "frontier-empty";
empty.textContent = "No tasks are currently ready to pull.";
frontierEl.append(empty);
} else {
const grid = document.createElement("div");
grid.className = "frontier-grid";
for (const item of items) grid.append(taskCard(item));
frontierEl.append(grid);
}
frontierEl.hidden = false;
}
function showEmpty() {
app.project = null;
emptyEl.hidden = false;
boardEl.hidden = true;
frontierEl.hidden = true;
$("#new-task").disabled = true;
}
function openTask(item) {
app.task = item;
$("#detail-id").textContent = `${item.id} · ${item.state}`;
$("#detail-title").textContent = item.title;
$("#detail-description").textContent = item.description || "No description provided.";
const meta = $("#detail-meta");
meta.replaceChildren();
for (const [label, value] of [["Repository", item.repo || "Project-wide"], ["Priority", item.priority], ["Owner", item.owner || "Unassigned"], ["Revision", item.revision]]) {
const group = document.createElement("div");
const term = document.createElement("dt");
term.textContent = label;
const description = document.createElement("dd");
description.textContent = value;
group.append(term, description);
meta.append(group);
}
const index = states.indexOf(item.state);
$("#move-back").disabled = index <= 0;
$("#move-next").disabled = index >= states.length - 1 || item.state === "Ready";
$("#move-next").textContent = index < states.length - 1 ? `Move to ${states[index + 1]}` : "Workflow complete";
$("#claim-task").hidden = item.state !== "Ready";
$("#detail-gate").textContent = item.state === "Ready" ? "Ready work must be claimed before execution starts." : item.blocked ? `Blocked: ${item.blockReason}` : "The server revalidates revision, dependencies, and evidence before moving.";
detailDialog.showModal();
}
async function mutateTask(path, body, success) {
try {
const item = await api(`/api/items/${app.task.id}/${path}`, { method: "POST", body: JSON.stringify(body) });
if (item?.leaseToken) {
app.leases[app.task.id] = { token: item.leaseToken, generation: item.leaseGeneration };
sessionStorage.setItem("akuraiTaskLeases", JSON.stringify(app.leases));
}
if (item?.id) app.task = item;
toast(success);
detailDialog.close();
await loadBoard();
} catch (error) {
if (!String(error.message).includes("Redirecting")) toast(error.message, "error");
}
}
$("#project-form").addEventListener("submit", async (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const payload = { key: data.get("key").trim(), name: data.get("name").trim(), repos: data.get("repos").split(/\r?\n/).map((repo) => repo.trim()).filter(Boolean) };
try {
await api("/api/projects", { method: "POST", body: JSON.stringify(payload) });
projectDialog.close();
event.currentTarget.reset();
toast(`Project ${payload.key} created`);
await loadProjects(payload.key);
} catch (error) {
toast(error.message, "error");
}
});
$("#task-form").addEventListener("submit", async (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const payload = { project: app.project.key, title: data.get("title").trim(), description: data.get("description").trim(), repo: data.get("repo") || null, priority: data.get("priority") };
try {
await api("/api/items", { method: "POST", body: JSON.stringify(payload) });
taskDialog.close();
event.currentTarget.reset();
toast("Task created in Inbox");
await loadBoard();
} catch (error) {
toast(error.message, "error");
}
});
$("#move-back").addEventListener("click", () => {
const index = states.indexOf(app.task.state);
mutateTask("transition", { state: states[index - 1], expectedRevision: app.task.revision }, `Moved to ${states[index - 1]}`);
});
$("#move-next").addEventListener("click", () => {
const index = states.indexOf(app.task.state);
mutateTask("transition", { state: states[index + 1], expectedRevision: app.task.revision }, `Moved to ${states[index + 1]}`);
});
$("#claim-task").addEventListener("click", () => mutateTask("claim", { expectedRevision: app.task.revision }, "Task claimed"));
$("#add-evidence").addEventListener("click", () => {
const input = $("#evidence-body");
if (!input.value.trim()) return toast("Evidence text is required", "error");
mutateTask("evidence", { body: input.value.trim() }, "Evidence attached");
input.value = "";
});
$("#new-project").addEventListener("click", () => projectDialog.showModal());
$("#sidebar-new-project").addEventListener("click", () => projectDialog.showModal());
$("#empty-create").addEventListener("click", () => projectDialog.showModal());
$("#new-task").addEventListener("click", () => taskDialog.showModal());
$("#session-button").addEventListener("click", () => window.location.assign("/auth/logout"));
for (const button of document.querySelectorAll(".close-dialog")) button.addEventListener("click", () => button.closest("dialog").close());
for (const dialog of document.querySelectorAll("dialog")) dialog.addEventListener("click", (event) => { if (event.target === dialog) dialog.close(); });
for (const button of document.querySelectorAll(".view-button")) button.addEventListener("click", async () => {
app.view = button.dataset.view;
document.querySelectorAll(".view-button").forEach((candidate) => candidate.classList.toggle("active", candidate === button));
await loadBoard();
});
boot();