AkurAI Build
Menu

akurai-tasks

public

Latest change d290a2d7ff4d058d6d8c6c7afa429bfaad419489 - Fix Tasks UI runtime and responsive shell by Ólafur Búi Ólafsson

const states = ["Inbox", "Triage", "Discovery", "Ready", "In Progress", "Review", "Verification", "Release Ready", "Done"];
let storedLeases = {};
try {
  storedLeases = JSON.parse(sessionStorage.getItem("akuraiTaskLeases") || "{}");
} catch {
  sessionStorage.removeItem("akuraiTaskLeases");
}
const app = {
  actor: "",
  leases: storedLeases,
  projects: [],
  project: null,
  board: null,
  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() {
  try {
    await 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 {
    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();
    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();