summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-03 21:15:55 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-03 21:15:55 +0000
commite8d1b80bd504088a7535e6045ab77f1ddd3b3d43 (patch)
treeb4eca65fc1306be5b56698941018969f90d716f7 /web
parent74cc740398cf2d90804ab19db728c844c2e056b7 (diff)
Web UI: tabs, new task modal with AI draft, templates panel
- Tab bar to switch between Tasks and Templates views - New Task modal with elaborate section ("Draft with AI") that calls POST /api/tasks/elaborate and pre-fills form fields - Templates panel listing saved configs with create/edit/delete - base-path meta tag for sub-path deployments - filter.test.mjs: contract tests for filterTasks function Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'web')
-rw-r--r--web/index.html105
-rw-r--r--web/test/filter.test.mjs77
2 files changed, 180 insertions, 2 deletions
diff --git a/web/index.html b/web/index.html
index cb2b670..6d7f23b 100644
--- a/web/index.html
+++ b/web/index.html
@@ -4,17 +4,118 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Claudomator</title>
+ <meta name="base-path" content="/claudomator">
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header>
<h1>Claudomator</h1>
+ <button id="btn-new-task" class="btn-primary">New Task</button>
</header>
+ <nav class="tab-bar">
+ <button class="tab active" data-tab="tasks">Tasks</button>
+ <button class="tab" data-tab="templates">Templates</button>
+ </nav>
<main id="app">
- <div class="task-list">
- <div id="loading">Loading tasks…</div>
+ <div data-panel="tasks">
+ <div class="task-list-toolbar">
+ <button id="btn-toggle-completed" class="btn-secondary btn-sm"></button>
+ </div>
+ <div class="task-list">
+ <div id="loading">Loading tasks…</div>
+ </div>
+ </div>
+ <div data-panel="templates" hidden>
+ <div class="panel-header">
+ <h2>Templates</h2>
+ <button id="btn-new-template" class="btn-primary">New Template</button>
+ </div>
+ <div class="template-list"></div>
</div>
</main>
+
+ <dialog id="task-modal">
+ <form id="task-form" method="dialog">
+ <h2>New Task</h2>
+ <div class="elaborate-section">
+ <label>Describe what you want Claude to do
+ <textarea id="elaborate-prompt" rows="3"
+ placeholder="e.g. run tests with race detector and check coverage"></textarea>
+ </label>
+ <button type="button" id="btn-elaborate" class="btn-secondary">
+ Draft with AI ✦
+ </button>
+ <p class="elaborate-hint">Claude will fill in the form fields below. You can edit before submitting.</p>
+ </div>
+ <hr class="form-divider">
+ <label>Name <input name="name" required></label>
+ <label>Instructions <textarea name="instructions" rows="6" required></textarea></label>
+ <label>Working Directory <input name="working_dir" placeholder="/path/to/repo"></label>
+ <label>Model <input name="model" value="sonnet"></label>
+ <label>Max Budget (USD) <input name="max_budget_usd" type="number" step="0.01" value="1.00"></label>
+ <label>Timeout <input name="timeout" value="15m"></label>
+ <label>Priority
+ <select name="priority">
+ <option value="normal" selected>Normal</option>
+ <option value="high">High</option>
+ <option value="low">Low</option>
+ </select>
+ </label>
+ <div class="form-actions">
+ <button type="button" id="btn-cancel-task">Cancel</button>
+ <button type="submit" class="btn-primary">Create &amp; Queue</button>
+ </div>
+ </form>
+ </dialog>
+
+ <dialog id="template-modal">
+ <form id="template-form" method="dialog">
+ <h2>New Template</h2>
+ <label>Name <input name="name" required></label>
+ <label>Description <textarea name="description" rows="2"></textarea></label>
+ <label>Model <input name="model" value="sonnet"></label>
+ <label>Instructions <textarea name="instructions" rows="6" required></textarea></label>
+ <label>Working Directory <input name="working_dir" placeholder="/path/to/repo"></label>
+ <label>Max Budget (USD) <input name="max_budget_usd" type="number" step="0.01" value="1.00"></label>
+ <label>Allowed Tools <input name="allowed_tools" placeholder="Bash, Read, Write"></label>
+ <label>Timeout <input name="timeout" value="15m"></label>
+ <label>Priority
+ <select name="priority">
+ <option value="normal" selected>Normal</option>
+ <option value="high">High</option>
+ <option value="low">Low</option>
+ </select>
+ </label>
+ <label>Tags <input name="tags" placeholder="ci, daily"></label>
+ <div class="form-actions">
+ <button type="button" id="btn-cancel-template">Cancel</button>
+ <button type="submit" class="btn-primary">Save Template</button>
+ </div>
+ </form>
+ </dialog>
+
+ <!-- Side panel backdrop -->
+ <div id="task-panel-backdrop" class="panel-backdrop" hidden></div>
+
+ <!-- Task detail side panel -->
+ <aside id="task-panel" class="task-panel">
+ <div class="task-panel-header">
+ <h2 id="task-panel-title">Task Details</h2>
+ <button id="btn-close-panel" class="btn-close-panel" aria-label="Close">&#x2715;</button>
+ </div>
+ <div id="task-panel-content" class="task-panel-content"></div>
+ </aside>
+
+ <!-- Execution detail modal -->
+ <dialog id="logs-modal">
+ <h2 id="logs-modal-title">Execution</h2>
+ <div id="logs-modal-body" class="logs-modal-body"></div>
+ <div class="form-actions" style="margin-top:1.25rem">
+ <span></span>
+ <button id="btn-close-logs" class="btn-primary">Close</button>
+ </div>
+ </dialog>
+
<script src="app.js" defer></script>
</body>
</html>
diff --git a/web/test/filter.test.mjs b/web/test/filter.test.mjs
new file mode 100644
index 0000000..947934b
--- /dev/null
+++ b/web/test/filter.test.mjs
@@ -0,0 +1,77 @@
+// filter.test.mjs — TDD contract tests for filterTasks
+//
+// filterTasks is defined inline here to establish the expected behaviour.
+// Once filterTasks is added to web/app.js (or a shared module), remove the
+// inline definition and import it instead.
+//
+// Run with: node --test web/test/filter.test.mjs
+
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
+
+// ── Implementation under contract ─────────────────────────────────────────────
+// Remove this block once filterTasks is exported from app.js / a shared module.
+
+const HIDE_STATES = new Set(['COMPLETED', 'FAILED']);
+
+function filterTasks(tasks, hideCompletedFailed = false) {
+ if (!hideCompletedFailed) return tasks;
+ return tasks.filter(t => !HIDE_STATES.has(t.state));
+}
+
+// ── Helpers ────────────────────────────────────────────────────────────────────
+
+function makeTask(state) {
+ return { id: state, name: `task-${state}`, state };
+}
+
+// ── Tests ──────────────────────────────────────────────────────────────────────
+
+describe('filterTasks', () => {
+ it('removes COMPLETED tasks when hideCompletedFailed=true', () => {
+ const tasks = [makeTask('COMPLETED'), makeTask('PENDING')];
+ const result = filterTasks(tasks, true);
+ assert.ok(!result.some(t => t.state === 'COMPLETED'), 'COMPLETED should be excluded');
+ assert.equal(result.length, 1);
+ });
+
+ it('removes FAILED tasks when hideCompletedFailed=true', () => {
+ const tasks = [makeTask('FAILED'), makeTask('RUNNING')];
+ const result = filterTasks(tasks, true);
+ assert.ok(!result.some(t => t.state === 'FAILED'), 'FAILED should be excluded');
+ assert.equal(result.length, 1);
+ });
+
+ it('returns all tasks when hideCompletedFailed=false', () => {
+ const tasks = [
+ makeTask('COMPLETED'),
+ makeTask('FAILED'),
+ makeTask('PENDING'),
+ makeTask('RUNNING'),
+ ];
+ const result = filterTasks(tasks, false);
+ assert.equal(result.length, 4, 'all tasks should be returned');
+ assert.deepEqual(result, tasks);
+ });
+
+ it('returns all tasks when hideCompletedFailed is omitted (default false)', () => {
+ const tasks = [makeTask('COMPLETED'), makeTask('FAILED'), makeTask('QUEUED')];
+ const result = filterTasks(tasks);
+ assert.equal(result.length, 3);
+ });
+
+ it('includes PENDING, QUEUED, RUNNING, TIMED_OUT, CANCELLED, BUDGET_EXCEEDED when hiding', () => {
+ const activeStates = ['PENDING', 'QUEUED', 'RUNNING', 'TIMED_OUT', 'CANCELLED', 'BUDGET_EXCEEDED'];
+ const tasks = activeStates.map(makeTask);
+ const result = filterTasks(tasks, true);
+ assert.equal(result.length, activeStates.length, 'all non-terminal active states should be kept');
+ for (const state of activeStates) {
+ assert.ok(result.some(t => t.state === state), `${state} should be included`);
+ }
+ });
+
+ it('returns an empty array when given an empty array', () => {
+ assert.deepEqual(filterTasks([], true), []);
+ assert.deepEqual(filterTasks([], false), []);
+ });
+});