diff options
Diffstat (limited to 'web')
| -rw-r--r-- | web/index.html | 105 | ||||
| -rw-r--r-- | web/test/filter.test.mjs | 77 |
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 & 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">✕</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), []); + }); +}); |
