Skip to content

Throwaway UIs

Daemon tasks can serve HTTP, turning any task into a lightweight web frontend. No framework, no build step, no deployment pipeline — just a task that serves HTML.

When to use this

  • Debug webhook payloads with a live-updating page
  • Collect team input with a quick poll or form
  • Build an approval gate with Accept/Reject buttons
  • Create a status dashboard from task run results
  • Run simulations with a parameter form

Webhook visualizer

A daemon task that receives webhooks and displays them in real time:

yaml
apiVersion: dicode/v1
kind: Task
name: webhook-viewer
runtime: deno
trigger:
  daemon: true
  restart: always
typescript
const events: unknown[] = [];

Deno.serve({ port: 3001 }, async (req) => {
  if (req.method === "POST") {
    events.push({
      time: new Date().toISOString(),
      body: await req.json(),
    });
    return new Response("ok");
  }

  return new Response(`
    <!DOCTYPE html>
    <html>
    <body style="font-family: system-ui; max-width: 800px; margin: 2rem auto;">
      <h1>Webhook Events (${events.length})</h1>
      <pre>${JSON.stringify(events.slice(-20), null, 2)}</pre>
      <script>setTimeout(() => location.reload(), 2000)</script>
    </body>
    </html>
  `, { headers: { "content-type": "text/html" } });
});

Point your webhook relay URL at this task and watch payloads arrive in real time.

Team poll

A quick poll backed by the built-in KV store:

yaml
apiVersion: dicode/v1
kind: Task
name: team-poll
runtime: deno
trigger:
  daemon: true
  restart: always
typescript
Deno.serve({ port: 3002 }, async (req) => {
  if (req.method === "POST") {
    const form = await req.formData();
    const choice = form.get("choice") as string;
    await kv.set(`vote:${Date.now()}`, choice);
    return Response.redirect("/");
  }

  const votes = await kv.list("vote:");
  const counts: Record<string, number> = {};
  for (const v of votes) {
    counts[v.value as string] = (counts[v.value as string] ?? 0) + 1;
  }

  return new Response(`
    <!DOCTYPE html>
    <html>
    <body style="font-family: system-ui; max-width: 600px; margin: 2rem auto; text-align: center;">
      <h2>Friday lunch?</h2>
      <form method="POST" style="display: flex; gap: 1rem; justify-content: center;">
        <button name="choice" value="pizza" style="font-size: 2rem; padding: 1rem 2rem; cursor: pointer;">
          Pizza (${counts["pizza"] ?? 0})
        </button>
        <button name="choice" value="sushi" style="font-size: 2rem; padding: 1rem 2rem; cursor: pointer;">
          Sushi (${counts["sushi"] ?? 0})
        </button>
      </form>
      <p style="color: #666; margin-top: 1rem;">${votes.length} votes total</p>
    </body>
    </html>
  `, { headers: { "content-type": "text/html" } });
});

Approval gate

A human-in-the-loop task that pauses execution until someone clicks Approve or Reject:

typescript
// approval-gate/task.ts
Deno.serve({ port: 3003 }, async (req) => {
  if (req.method === "POST") {
    const form = await req.formData();
    const decision = form.get("decision") as string;
    await kv.set("approval", decision);
    // Trigger the downstream task
    if (decision === "approve") {
      await dicode.run_task("deploy-production");
    }
    return new Response(`<h2>${decision === "approve" ? "Approved" : "Rejected"}</h2>`, {
      headers: { "content-type": "text/html" },
    });
  }

  return new Response(`
    <h2>Deploy to production?</h2>
    <form method="POST" style="display: flex; gap: 1rem;">
      <button name="decision" value="approve" style="background: #22c55e; color: white; padding: .8rem 2rem; border: none; border-radius: 8px; cursor: pointer;">Approve</button>
      <button name="decision" value="reject" style="background: #ef4444; color: white; padding: .8rem 2rem; border: none; border-radius: 8px; cursor: pointer;">Reject</button>
    </form>
  `, { headers: { "content-type": "text/html" } });
});

Status dashboard

A daemon task that aggregates results from other tasks into a custom status page:

typescript
// status-dashboard/task.ts
Deno.serve({ port: 3004 }, async () => {
  const tasks = await dicode.list_tasks();
  const rows = [];

  for (const t of tasks.slice(0, 20)) {
    const runs = await dicode.get_runs(t.id, { limit: 1 });
    const last = runs[0];
    const status = !last ? "no runs" : last.success ? "ok" : "failed";
    const color = status === "ok" ? "#22c55e" : status === "failed" ? "#ef4444" : "#666";
    rows.push(`
      <tr>
        <td>${t.name}</td>
        <td style="color:${color}; font-weight:600;">${status}</td>
        <td>${last?.finished_at ?? "—"}</td>
        <td>${last?.duration_ms ? last.duration_ms + "ms" : "—"}</td>
      </tr>
    `);
  }

  return new Response(`
    <!DOCTYPE html>
    <html>
    <body style="font-family: system-ui; max-width: 900px; margin: 2rem auto;">
      <h1>Task Status Dashboard</h1>
      <table style="width:100%; border-collapse:collapse;">
        <thead>
          <tr style="border-bottom:2px solid #333; text-align:left;">
            <th>Task</th><th>Status</th><th>Last Run</th><th>Duration</th>
          </tr>
        </thead>
        <tbody>${rows.join("")}</tbody>
      </table>
      <p style="color:#666; margin-top:1rem; font-size:.85rem;">
        Auto-refreshes every 30 seconds.
      </p>
      <script>setTimeout(() => location.reload(), 30000)</script>
    </body>
    </html>
  `, { headers: { "content-type": "text/html" } });
});

This dashboard calls dicode.list_tasks() and dicode.get_runs() to build a real-time status table. Add permissions.dicode.list_tasks: true and permissions.dicode.get_runs: true to the task.yaml.

Key points

  • Throwaway UIs are just daemon tasks — they get git versioning, permissions, secrets, KV store, and run history for free
  • Delete the task folder and the UI is gone — no cleanup needed
  • Combine with the webhook relay for public-facing forms behind NAT
  • Use dicode.run_task() to trigger other tasks from button clicks