Menu
akurai-tasks
publicLatest change 443cb3d267694cdccc9afbac6d6ae92c059db94a - AKURAI-TASKS-3 AKURAI-TASKS-4 AKURAI-TASKS-5 PIBUN-42 webhook sender by Ólafur Búi Ólafsson
let workflowStates = [];
let storedLeases = {};
try {
storedLeases = JSON.parse(sessionStorage.getItem("akuraiTaskLeases") || "{}");
} catch {
sessionStorage.removeItem("akuraiTaskLeases");
}
const app = {
actor: "",
leases: storedLeases,
projects: [],
project: null,
board: null,
workflow: workflowStates,
task: null,
view: localStorage.getItem("akuraiTasksView") || "board",
};
localStorage.setItem("akuraiTasksView", app.view);
sessionStorage.setItem("akuraiTaskLeases", JSON.stringify(app.leases));
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");
const loginScreen = $("#login-screen");
const sidebarEl = $("#project-sidebar");
const sidebarToggle = $("#sidebar-toggle");
const mobileNavigation = window.matchMedia("(max-width: 800px)");
let sidebarTrigger = null;
function closeSidebar(restoreFocus = true) {
document.body.classList.remove("sidebar-open");
sidebarToggle.setAttribute("aria-expanded", "false");
$("#sidebar-scrim").tabIndex = -1;
if (mobileNavigation.matches) {
sidebarEl.inert = true;
sidebarEl.setAttribute("aria-hidden", "true");
}
if (restoreFocus) {
const target = sidebarTrigger?.isConnected ? sidebarTrigger : sidebarToggle;
target.focus();
}
sidebarTrigger = null;
}
function openSidebar(trigger, focusAccount = false) {
if (!mobileNavigation.matches) {
(focusAccount ? $("#logout-button") : sidebarEl).focus();
return;
}
sidebarTrigger = trigger;
document.body.classList.add("sidebar-open");
sidebarToggle.setAttribute("aria-expanded", "true");
sidebarEl.inert = false;
sidebarEl.setAttribute("aria-hidden", "false");
$("#sidebar-scrim").tabIndex = 0;
requestAnimationFrame(() => (focusAccount ? $("#logout-button") : $("#sidebar-close")).focus());
}
function syncSidebarMode() {
document.body.classList.remove("sidebar-open");
sidebarToggle.setAttribute("aria-expanded", "false");
$("#sidebar-scrim").tabIndex = -1;
sidebarTrigger = null;
sidebarEl.inert = mobileNavigation.matches;
if (mobileNavigation.matches) sidebarEl.setAttribute("aria-hidden", "true");
else sidebarEl.removeAttribute("aria-hidden");
}
syncSidebarMode();
mobileNavigation.addEventListener("change", syncSidebarMode);
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.ok) {
const error = new Error(payload.error || `Request failed with ${response.status}`);
error.status = response.status;
throw error;
}
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() {
await api("/api/health");
setHealth(true, "Service ready");
} catch {
setHealth(false, "Service unavailable");
}
try {
const session = await api("/api/v1/session");
if (!session.authenticated) {
loginScreen.hidden = false;
$("#login-button").focus();
return;
}
app.actor = typeof session.principal === "string" ? session.principal.trim() || "AkurAI user" : "AkurAI user";
loginScreen.hidden = true;
$(".topbar").inert = false;
$(".workspace").inert = false;
const actorInitial = app.actor.charAt(0).toUpperCase();
$("#session-avatar").textContent = actorInitial;
$("#sidebar-avatar").textContent = actorInitial;
$("#sidebar-user").textContent = app.actor;
$("#session-button").title = `Signed in as ${app.actor}`;
document.querySelectorAll(".view-button").forEach((button) => button.classList.toggle("active", button.dataset.view === app.view));
await loadProjects(localStorage.getItem("akuraiTasksProject"));
} catch (error) {
toast(error.message, "error");
}
}
async function loadProjects(selectKey) {
try {
app.projects = await api("/api/projects");
renderProjects();
const preferred = selectKey || app.project?.key;
const key = app.projects.some((project) => project.key === preferred) ? preferred : 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.dataset.key = project.key;
const active = app.project?.key === project.key;
button.classList.toggle("active", active);
if (active) button.setAttribute("aria-current", "page");
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", async () => {
await selectProject(project.key);
if (mobileNavigation.matches) closeSidebar();
});
projectsEl.append(button);
}
}
async function selectProject(key) {
app.project = app.projects.find((project) => project.key === key);
localStorage.setItem("akuraiTasksProject", 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();
if (app.project.repos.length > 1) {
const placeholder = new Option("Choose repository", "", true, true);
placeholder.disabled = true;
select.add(placeholder);
select.required = true;
} else {
select.add(new Option("Project-wide", ""));
select.required = false;
}
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 {
app.board = await api(`/api/projects/${app.project.key}/board`);
app.workflow = app.board.workflow.states;
if (app.view === "frontier") {
const items = await api(`/api/projects/${app.project.key}/frontier`);
renderFrontier(items);
} else {
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 = app.workflow.indexOf(item.state);
const claimState = app.board.workflow.claimState;
$("#move-back").disabled = index <= 0;
$("#move-next").disabled = index >= app.workflow.length - 1 || item.state === claimState;
$("#move-next").textContent = index < app.workflow.length - 1 ? `Move to ${app.workflow[index + 1]}` : "Workflow complete";
$("#claim-task").hidden = item.state !== claimState;
$("#detail-gate").textContent = item.state === claimState ? "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();
return true;
} catch (error) {
if (error.status === 403 && String(error.message).includes("lease")) {
delete app.leases[app.task.id];
sessionStorage.setItem("akuraiTaskLeases", JSON.stringify(app.leases));
}
if (!String(error.message).includes("Redirecting")) toast(error.message, "error");
return false;
}
}
$("#project-form").addEventListener("submit", async (event) => {
event.preventDefault();
const form = event.currentTarget;
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();
form.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 form = event.currentTarget;
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();
form.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", async () => {
const input = $("#evidence-body");
const evidence = input.value.trim();
if (!evidence) return toast("Evidence text is required", "error");
if (await mutateTask("evidence", { body: evidence }, "Evidence attached")) input.value = "";
});
$("#login-button").addEventListener("click", () => window.location.assign("/auth/login"));
const openProjectDialog = () => {
closeSidebar(false);
projectDialog.showModal();
};
$("#new-project").addEventListener("click", openProjectDialog);
$("#sidebar-new-project").addEventListener("click", openProjectDialog);
$("#empty-create").addEventListener("click", openProjectDialog);
$("#new-task").addEventListener("click", () => taskDialog.showModal());
sidebarToggle.addEventListener("click", () => openSidebar(sidebarToggle));
$("#sidebar-close").addEventListener("click", () => closeSidebar());
$("#sidebar-scrim").addEventListener("click", () => closeSidebar());
$("#session-button").addEventListener("click", (event) => openSidebar(event.currentTarget, true));
$("#logout-button").addEventListener("click", () => {
sessionStorage.removeItem("akuraiTaskLeases");
window.location.assign("/auth/logout");
});
document.addEventListener("keydown", (event) => {
if (!document.body.classList.contains("sidebar-open")) return;
if (event.key === "Escape") {
event.preventDefault();
closeSidebar();
return;
}
if (event.key !== "Tab") return;
const focusable = [...sidebarEl.querySelectorAll("button:not(:disabled), [href], [tabindex]:not([tabindex='-1'])")];
if (!focusable.length) return;
const first = focusable[0];
const last = focusable.at(-1);
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
});
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;
localStorage.setItem("akuraiTasksView", app.view);
document.querySelectorAll(".view-button").forEach((candidate) => candidate.classList.toggle("active", candidate === button));
await loadBoard();
});
boot();