BranchDashboard: Web

Dashboard: Web

A localhost HTML page that polls the CLI. Simplest stack.

The simplest dashboard. One HTML file, a tiny Bun server, no framework.

The Server

Create src/dashboard-server.ts — 15 lines that proxy CLI commands as HTTP:

import { join, resolve } from "node:path";

const ROOT = resolve(import.meta.dir, "..");
const CLI = join(ROOT, "src/cli.ts");

Bun.serve({
  port: 3847,
  async fetch(req) {
    const url = new URL(req.url);

    if (url.pathname.startsWith("/api/")) {
      const args = url.pathname.replace("/api/", "").split("/").filter(Boolean);
      const proc = Bun.spawnSync(["bun", "run", CLI, ...args]);
      return new Response(new TextDecoder().decode(proc.stdout), {
        headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
      });
    }

    if (url.pathname === "/") return new Response(Bun.file(join(ROOT, "dashboard/index.html")));
    return new Response("Not found", { status: 404 });
  },
});

console.log("Dashboard: http://localhost:3847");

The HTML

Create dashboard/index.html. The CLI returns JSON — just fetch and render:

async function load() {
  const resp = await fetch('/api/status');
  const data = await resp.json();
  // data.result.jobs is an array of {name, schedule, loaded, exitCode, ...}
  // Render however you want
}

load();
setInterval(load, 5000);

Your coding agent can build the full HTML for you. Ask it: "build a dark-theme dashboard HTML page that fetches from /api/status and renders the jobs as cards"

Test It

bun run src/dashboard-server.ts
# Open http://localhost:3847

Companion Notes

Branch: Web Dashboard (localhost)

The simplest dashboard. A single HTML file that polls the CLI and renders job state. No build step, no framework, no dependencies.

Architecture

browser → fetch http://localhost:3847/api/status

        bun server (5 lines) → shells out to `jobs status`

        returns JSON → browser renders it

Step 1: The Server

Create src/dashboard-server.ts:

#!/usr/bin/env bun
import { resolve, join } from "node:path";

const PROJECT_ROOT = resolve(import.meta.dir, "..");
const PORT = 3847;

Bun.serve({
  port: PORT,
  async fetch(req) {
    const url = new URL(req.url);

    // API: proxy CLI commands
    if (url.pathname.startsWith("/api/")) {
      const command = url.pathname.replace("/api/", "");
      const args = command ? command.split("/") : [];
      const proc = Bun.spawnSync(["bun", "run", join(PROJECT_ROOT, "src/cli.ts"), ...args]);
      const stdout = new TextDecoder().decode(proc.stdout);

      return new Response(stdout, {
        headers: {
          "Content-Type": "application/json",
          "Access-Control-Allow-Origin": "*",
        },
      });
    }

    // Serve the dashboard HTML
    if (url.pathname === "/" || url.pathname === "/index.html") {
      return new Response(Bun.file(join(PROJECT_ROOT, "dashboard/index.html")));
    }

    return new Response("Not found", { status: 404 });
  },
});

console.log(`Dashboard: http://localhost:${PORT}`);

Step 2: The HTML

Create dashboard/index.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>AI Job Scheduler</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: system-ui; background: #0a0a0a; color: #e0e0e0; padding: 2rem; }
    h1 { font-size: 1.2rem; color: #888; margin-bottom: 1.5rem; }
    .jobs { display: grid; gap: 1rem; max-width: 800px; }
    .job { background: #161616; border: 1px solid #222; border-radius: 8px; padding: 1rem 1.25rem; }
    .job-header { display: flex; justify-content: space-between; align-items: center; }
    .job-name { font-weight: 600; font-size: 1rem; }
    .job-status { font-size: 0.8rem; padding: 2px 8px; border-radius: 4px; }
    .status-loaded { background: #0a2a0a; color: #4ade80; border: 1px solid #166534; }
    .status-unloaded { background: #2a1a0a; color: #fbbf24; border: 1px solid #854d0e; }
    .status-error { background: #2a0a0a; color: #f87171; border: 1px solid #991b1b; }
    .job-meta { font-size: 0.8rem; color: #666; margin-top: 0.5rem; }
    .job-meta span { margin-right: 1.5rem; }
    .refresh { font-size: 0.75rem; color: #444; margin-top: 1rem; }
  </style>
</head>
<body>
  <h1>AI Job Scheduler</h1>
  <div id="jobs" class="jobs"></div>
  <div id="refresh" class="refresh"></div>

  <script>
    async function load() {
      try {
        const resp = await fetch('/api/status');
        const data = await resp.json();

        const container = document.getElementById('jobs');
        container.innerHTML = data.result.jobs.map(job => {
          const statusClass = !job.loaded ? 'status-unloaded'
            : (job.exitCode !== '0' && job.exitCode !== '-') ? 'status-error'
            : 'status-loaded';
          const statusText = !job.loaded ? 'unloaded'
            : (job.exitCode !== '0' && job.exitCode !== '-') ? 'error'
            : job.pid !== '-' ? 'running' : 'idle';

          return `<div class="job">
            <div class="job-header">
              <span class="job-name">${job.name}</span>
              <span class="job-status ${statusClass}">${statusText}</span>
            </div>
            <div class="job-meta">
              <span>${job.schedule}</span>
              <span>${job.scope}</span>
              <span>exit: ${job.exitCode}</span>
            </div>
          </div>`;
        }).join('');

        document.getElementById('refresh').textContent =
          `${data.result.total} jobs · updated ${new Date().toLocaleTimeString()}`;
      } catch (err) {
        document.getElementById('jobs').innerHTML =
          `<div class="job" style="border-color: #991b1b">Connection error: ${err.message}</div>`;
      }
    }

    load();
    setInterval(load, 5000);
  </script>
</body>
</html>

Step 3: Run It

bun run src/dashboard-server.ts
# → Dashboard: http://localhost:3847

Add a script to package.json: "dashboard": "bun run src/dashboard-server.ts"

What to Add Next

  • Click a job → show logs (fetch /api/logs/JOB_NAME)
  • Click a job → kick it (fetch /api/kick/JOB_NAME)
  • Doctor status as a health bar
  • Auto-refresh with visual diff (highlight changed jobs)