summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-05-25 05:12:54 +0000
committerClaude <noreply@anthropic.com>2026-05-25 05:12:54 +0000
commit5669e2ca58b4a99616083eaff9f639ecce4ba809 (patch)
treea50bf5ee04f10c8dd463ffe9837d6818c5877993
parent42bb6432d3e53042723b43330c22eafbf553e10f (diff)
refactor(web): remove the stories dashboard from the UI (Phase 3)
The observability UI drops the stories surface: the Stories tab, the stories panel, the New/Detail story modals, and all their app.js code (STORY_STATUS_LABELS, storyStatusLabel, renderStoryCard, renderStoriesPanel, openStoryDetail, openStoryModal, renderElaboratedPlan, the story-modal init handlers, and the panel-dispatch 'stories' case) plus stories.test.mjs. The stories BACKEND (table, /api/stories* handlers, story-elaborate) is left intact for the Phase 8 cleanup pass; this only removes the UI surface. Dead CSS for the removed elements is likewise deferred to Phase 8. Verified with node --test (251 JS tests pass) and a module-import check. Not browser-verified in this environment. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
-rw-r--r--web/app.js383
-rw-r--r--web/index.html32
-rw-r--r--web/test/stories.test.mjs164
3 files changed, 0 insertions, 579 deletions
diff --git a/web/app.js b/web/app.js
index 0a66a11..8a2662f 100644
--- a/web/app.js
+++ b/web/app.js
@@ -611,90 +611,6 @@ export function computeExecutionStats(executions) {
};
}
-// ── Stories ───────────────────────────────────────────────────────────────────
-
-const STORY_STATUS_LABELS = {
- PENDING: 'Pending',
- IN_PROGRESS: 'In Progress',
- SHIPPABLE: 'Shippable',
- DEPLOYED: 'Deployed',
- VALIDATING: 'Validating',
- REVIEW_READY: 'Review Ready',
- NEEDS_FIX: 'Needs Fix',
-};
-
-export function storyStatusLabel(status) {
- return STORY_STATUS_LABELS[status] || status;
-}
-
-export function renderStoryCard(story, doc = document) {
- const card = doc.createElement('div');
- card.className = 'story-card';
- card.dataset.storyId = story.id;
-
- const header = doc.createElement('div');
- header.className = 'story-card-header';
-
- const name = doc.createElement('span');
- name.className = 'story-name';
- name.textContent = story.name;
- header.appendChild(name);
-
- const badge = doc.createElement('span');
- badge.className = 'story-status-badge';
- badge.dataset.status = story.status;
- badge.textContent = storyStatusLabel(story.status);
- header.appendChild(badge);
-
- card.appendChild(header);
-
- const meta = doc.createElement('div');
- meta.className = 'story-meta';
-
- const project = doc.createElement('span');
- project.className = 'story-project';
- project.textContent = story.project_id || '—';
- meta.appendChild(project);
-
- if (story.branch_name) {
- const branch = doc.createElement('span');
- branch.className = 'story-branch';
- branch.textContent = story.branch_name;
- meta.appendChild(branch);
- }
-
- card.appendChild(meta);
-
- // Ship button for SHIPPABLE stories.
- if (story.status === 'SHIPPABLE') {
- const shipBtn = doc.createElement('button');
- shipBtn.className = 'btn-primary story-ship-btn';
- shipBtn.textContent = 'Ship';
- shipBtn.addEventListener('click', async (e) => {
- e.stopPropagation();
- shipBtn.disabled = true;
- shipBtn.textContent = 'Shipping…';
- try {
- const res = await fetch(`${API_BASE}/api/stories/${story.id}/ship`, { method: 'POST' });
- if (!res.ok) {
- const body = await res.json().catch(() => ({}));
- alert(body.error || `Ship failed (${res.status})`);
- shipBtn.disabled = false;
- shipBtn.textContent = 'Ship';
- } else {
- renderStoriesPanel();
- }
- } catch {
- shipBtn.disabled = false;
- shipBtn.textContent = 'Ship';
- }
- });
- card.appendChild(shipBtn);
- }
-
- return card;
-}
-
export function updateFilterTabs() {
const current = getTaskFilterTab();
document.querySelectorAll('.filter-tab[data-filter]').forEach(el => {
@@ -1370,9 +1286,6 @@ function renderActiveTab(allTasks) {
.then(([execs, agentData, dashStats]) => renderStatsPanel(allTasks, execs, agentData, dashStats))
.catch(() => {});
break;
- case 'stories':
- renderStoriesPanel();
- break;
case 'drops':
renderDropsPanel();
break;
@@ -3073,217 +2986,6 @@ async function fetchDrops() {
// ── Stories panel ─────────────────────────────────────────────────────────────
-async function renderStoriesPanel() {
- const panel = document.querySelector('[data-panel="stories"]');
- if (!panel) return;
-
- let stories;
- try {
- const res = await fetch(`${BASE_PATH}/api/stories`);
- stories = res.ok ? await res.json() : [];
- } catch {
- panel.innerHTML = '<p class="task-meta" style="padding:1rem">Failed to load stories.</p>';
- return;
- }
-
- panel.innerHTML = '';
-
- const toolbar = document.createElement('div');
- toolbar.className = 'stories-toolbar';
- const btnNew = document.createElement('button');
- btnNew.className = 'btn-primary';
- btnNew.textContent = 'New Story';
- btnNew.addEventListener('click', openStoryModal);
- toolbar.appendChild(btnNew);
- panel.appendChild(toolbar);
-
- if (!stories || stories.length === 0) {
- const empty = document.createElement('p');
- empty.className = 'task-empty';
- empty.textContent = 'No stories yet. Create one to get started.';
- panel.appendChild(empty);
- return;
- }
-
- const list = document.createElement('div');
- list.className = 'stories-list';
- for (const story of stories) {
- const card = renderStoryCard(story);
- card.addEventListener('click', () => openStoryDetail(story));
- list.appendChild(card);
- }
- panel.appendChild(list);
-}
-
-function openStoryDetail(story) {
- const modal = document.getElementById('story-detail-modal');
- if (!modal) return;
-
- document.getElementById('story-detail-name').textContent = story.name;
-
- const body = document.getElementById('story-detail-body');
- body.innerHTML = '';
-
- function addRow(label, value) {
- const row = document.createElement('div');
- row.className = 'meta-item';
- const lbl = document.createElement('div');
- lbl.className = 'meta-label';
- lbl.textContent = label;
- const val = document.createElement('div');
- val.className = 'meta-value';
- val.textContent = value || '—';
- row.appendChild(lbl);
- row.appendChild(val);
- body.appendChild(row);
- }
-
- const badge = document.createElement('span');
- badge.className = 'story-status-badge';
- badge.dataset.status = story.status;
- badge.textContent = storyStatusLabel(story.status);
-
- const statusRow = document.createElement('div');
- statusRow.className = 'meta-item';
- const statusLbl = document.createElement('div');
- statusLbl.className = 'meta-label';
- statusLbl.textContent = 'Status';
- statusRow.appendChild(statusLbl);
- statusRow.appendChild(badge);
- body.appendChild(statusRow);
-
- addRow('Project', story.project_id);
- addRow('Branch', story.branch_name);
- addRow('Created', story.created_at ? new Date(story.created_at).toLocaleString() : '—');
-
- // Load tasks for this story.
- const tasksSection = document.createElement('div');
- tasksSection.className = 'story-detail-tasks';
- tasksSection.innerHTML = '<p class="task-meta">Loading tasks…</p>';
- body.appendChild(tasksSection);
-
- fetch(`${API_BASE}/api/stories/${story.id}/tasks`)
- .then(r => r.ok ? r.json() : [])
- .then(async tasks => {
- tasksSection.innerHTML = '';
- const topLevel = tasks.filter(t => !t.parent_task_id);
- if (topLevel.length === 0) {
- tasksSection.innerHTML = '<p class="task-meta">No tasks yet.</p>';
- return;
- }
- const ol = document.createElement('ol');
- ol.className = 'story-detail-task-list';
- for (const t of topLevel) {
- const li = document.createElement('li');
- li.className = `story-detail-task story-detail-task-${t.state.toLowerCase()}`;
- li.textContent = `${STATE_EMOJI[t.state] || '•'} ${t.name}`;
- const subs = tasks.filter(s => s.parent_task_id === t.id);
- if (subs.length > 0) {
- const ul = document.createElement('ul');
- ul.className = 'story-detail-subtask-list';
- for (const s of subs) {
- const sli = document.createElement('li');
- sli.className = `subtask-item subtask-${s.state.toLowerCase()}`;
- sli.textContent = `${STATE_EMOJI[s.state] || '•'} ${s.name}`;
- ul.appendChild(sli);
- }
- li.appendChild(ul);
- }
- ol.appendChild(li);
- }
- tasksSection.appendChild(ol);
- })
- .catch(() => { tasksSection.innerHTML = '<p class="task-meta">Could not load tasks.</p>'; });
-
- modal.showModal();
-}
-
-function openStoryModal() {
- const modal = document.getElementById('story-modal');
- if (!modal) return;
-
- // Reset form state
- document.getElementById('story-goal').value = '';
- const planArea = document.getElementById('story-plan-area');
- planArea.innerHTML = '';
- planArea.setAttribute('hidden', '');
-
- const btnElaborate = document.getElementById('btn-story-elaborate');
- btnElaborate.disabled = false;
- btnElaborate.textContent = 'Elaborate with AI ✦';
-
- const btnApprove = document.getElementById('btn-story-approve');
- btnApprove.setAttribute('hidden', '');
- btnApprove._elaboratedPlan = null;
-
- // Populate project dropdown
- fetch(`${BASE_PATH}/api/projects`)
- .then(r => r.ok ? r.json() : [])
- .then(projects => {
- const sel = document.getElementById('story-project');
- sel.innerHTML = '';
- for (const p of projects) {
- const opt = document.createElement('option');
- opt.value = p.id;
- opt.textContent = p.name;
- sel.appendChild(opt);
- }
- })
- .catch(() => {});
-
- modal.showModal();
-}
-
-function renderElaboratedPlan(plan) {
- const planArea = document.getElementById('story-plan-area');
- planArea.innerHTML = '';
- planArea.removeAttribute('hidden');
-
- const nameEl = document.createElement('p');
- nameEl.className = 'story-plan-name';
- nameEl.textContent = `Story: ${plan.name}`;
- planArea.appendChild(nameEl);
-
- if (plan.branch_name) {
- const branchEl = document.createElement('p');
- branchEl.className = 'story-plan-branch';
- branchEl.textContent = `Branch: ${plan.branch_name}`;
- planArea.appendChild(branchEl);
- }
-
- if (plan.tasks && plan.tasks.length > 0) {
- const tasksHeader = document.createElement('p');
- tasksHeader.className = 'story-plan-section';
- tasksHeader.textContent = `Tasks (${plan.tasks.length}):`;
- planArea.appendChild(tasksHeader);
-
- const taskList = document.createElement('ol');
- taskList.className = 'story-plan-tasks';
- for (const t of plan.tasks) {
- const li = document.createElement('li');
- li.textContent = t.name;
- if (t.subtasks && t.subtasks.length > 0) {
- const subList = document.createElement('ul');
- for (const s of t.subtasks) {
- const subLi = document.createElement('li');
- subLi.textContent = s.name;
- subList.appendChild(subLi);
- }
- li.appendChild(subList);
- }
- taskList.appendChild(li);
- }
- planArea.appendChild(taskList);
- }
-
- if (plan.validation && plan.validation.type) {
- const valHeader = document.createElement('p');
- valHeader.className = 'story-plan-section';
- valHeader.textContent = `Validation: ${plan.validation.type}`;
- planArea.appendChild(valHeader);
- }
-}
-
async function renderDropsPanel() {
const panel = document.querySelector('[data-panel="drops"] .drops-panel');
if (!panel) return;
@@ -3404,90 +3106,5 @@ if (typeof document !== 'undefined') {
}
- // Story modal
- const storyModal = document.getElementById('story-modal');
- if (storyModal) {
- document.getElementById('btn-close-story-modal').addEventListener('click', () => storyModal.close());
-
- document.getElementById('btn-story-elaborate').addEventListener('click', async () => {
- const btn = document.getElementById('btn-story-elaborate');
- const goal = document.getElementById('story-goal').value.trim();
- const projectId = document.getElementById('story-project').value;
-
- if (!goal) {
- const errEl = document.createElement('p');
- errEl.className = 'form-error';
- errEl.textContent = 'Please enter a goal before elaborating.';
- storyModal.querySelector('.story-modal-body').appendChild(errEl);
- return;
- }
-
- storyModal.querySelectorAll('.form-error').forEach(el => el.remove());
- btn.disabled = true;
- btn.textContent = 'Elaborating…';
-
- try {
- const res = await fetch(`${BASE_PATH}/api/stories/elaborate`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ goal, project_id: projectId }),
- });
- if (!res.ok) {
- const err = await res.json().catch(() => ({ error: res.statusText }));
- throw new Error(err.error || res.statusText);
- }
- const plan = await res.json();
- renderElaboratedPlan(plan);
-
- const btnApprove = document.getElementById('btn-story-approve');
- btnApprove._elaboratedPlan = { ...plan, project_id: projectId };
- btnApprove.removeAttribute('hidden');
- } catch (err) {
- const errEl = document.createElement('p');
- errEl.className = 'form-error';
- errEl.textContent = `Elaboration failed: ${err.message}`;
- storyModal.querySelector('.story-modal-body').appendChild(errEl);
- } finally {
- btn.disabled = false;
- btn.textContent = 'Elaborate with AI ✦';
- }
- });
-
- document.getElementById('btn-story-approve').addEventListener('click', async () => {
- const btn = document.getElementById('btn-story-approve');
- const plan = btn._elaboratedPlan;
- if (!plan) return;
-
- btn.disabled = true;
- btn.textContent = 'Approving…';
-
- try {
- const res = await fetch(`${BASE_PATH}/api/stories/approve`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(plan),
- });
- if (!res.ok) {
- const err = await res.json().catch(() => ({ error: res.statusText }));
- throw new Error(err.error || res.statusText);
- }
- storyModal.close();
- renderStoriesPanel();
- } catch (err) {
- const errEl = document.createElement('p');
- errEl.className = 'form-error';
- errEl.textContent = `Approve failed: ${err.message}`;
- storyModal.querySelector('.story-modal-body').appendChild(errEl);
- btn.disabled = false;
- btn.textContent = 'Approve & Queue';
- }
- });
- }
-
- // Story detail modal
- const storyDetailModal = document.getElementById('story-detail-modal');
- if (storyDetailModal) {
- document.getElementById('btn-close-story-detail').addEventListener('click', () => storyDetailModal.close());
- }
});
}
diff --git a/web/index.html b/web/index.html
index d3bf81b..8a705cc 100644
--- a/web/index.html
+++ b/web/index.html
@@ -39,7 +39,6 @@
</div>
</header>
<nav class="tab-bar">
- <button class="tab" data-tab="stories" title="Stories">📖</button>
<button class="tab active" data-tab="queue" title="Queue">⏳</button>
<button class="tab" data-tab="interrupted" title="Interrupted">⚠️<span class="tab-count-badge" hidden></span></button>
<button class="tab" data-tab="ready" title="Ready">✅<span class="tab-count-badge" hidden></span></button>
@@ -68,7 +67,6 @@
<div data-panel="drops" hidden>
<div class="drops-panel"></div>
</div>
- <div data-panel="stories" hidden></div>
<div data-panel="stats" hidden></div>
<div data-panel="settings" hidden>
<p class="task-meta" style="padding:1rem">Settings coming soon.</p>
@@ -97,36 +95,6 @@
</div>
</dialog>
- <!-- New Story modal -->
- <dialog id="story-modal">
- <div class="story-modal-header">
- <h2>New Story</h2>
- <button id="btn-close-story-modal" class="btn-close-panel" aria-label="Close">&#x2715;</button>
- </div>
- <div class="story-modal-body">
- <label>Project
- <select id="story-project"></select>
- </label>
- <label>Goal
- <textarea id="story-goal" rows="4" placeholder="Describe the feature or change you want to build…"></textarea>
- </label>
- <button type="button" id="btn-story-elaborate" class="btn-secondary">Elaborate with AI ✦</button>
- <div id="story-plan-area" hidden></div>
- <div class="form-actions">
- <button type="button" id="btn-story-approve" class="btn-primary" hidden>Approve &amp; Queue</button>
- </div>
- </div>
- </dialog>
-
- <!-- Story detail modal -->
- <dialog id="story-detail-modal">
- <div class="story-modal-header">
- <h2 id="story-detail-name">Story</h2>
- <button id="btn-close-story-detail" class="btn-close-panel" aria-label="Close">&#x2715;</button>
- </div>
- <div id="story-detail-body" class="story-detail-body meta-grid"></div>
- </dialog>
-
<script type="module" src="app.js"></script>
</body>
</html>
diff --git a/web/test/stories.test.mjs b/web/test/stories.test.mjs
deleted file mode 100644
index 263f202..0000000
--- a/web/test/stories.test.mjs
+++ /dev/null
@@ -1,164 +0,0 @@
-// stories.test.mjs — TDD tests for stories UI functions.
-//
-// Run with: node --test web/test/stories.test.mjs
-
-import { describe, it, beforeEach } from 'node:test';
-import assert from 'node:assert/strict';
-import { renderStoryCard, storyStatusLabel } from '../app.js';
-
-// ── Minimal DOM mock ──────────────────────────────────────────────────────────
-
-function makeMockDoc() {
- function createElement(tag) {
- return {
- tag,
- className: '',
- textContent: '',
- innerHTML: '',
- hidden: false,
- dataset: {},
- children: [],
- _listeners: {},
- style: {},
- appendChild(child) { this.children.push(child); return child; },
- prepend(...nodes) { this.children.unshift(...nodes); },
- append(...nodes) { nodes.forEach(n => this.children.push(n)); },
- addEventListener(ev, fn) { this._listeners[ev] = fn; },
- querySelector(sel) {
- const cls = sel.startsWith('.') ? sel.slice(1) : null;
- function search(el) {
- if (cls && el.className && el.className.split(' ').includes(cls)) return el;
- for (const c of el.children || []) {
- const found = search(c);
- if (found) return found;
- }
- return null;
- }
- return search(this);
- },
- querySelectorAll(sel) {
- const cls = sel.startsWith('.') ? sel.slice(1) : null;
- const results = [];
- function search(el) {
- if (cls && el.className && el.className.split(' ').includes(cls)) results.push(el);
- for (const c of el.children || []) search(c);
- }
- search(this);
- return results;
- },
- };
- }
- return { createElement };
-}
-
-function makeStory(overrides = {}) {
- return {
- id: 'story-1',
- name: 'Add login page',
- project_id: 'claudomator',
- branch_name: 'story/add-login-page',
- status: 'PENDING',
- created_at: '2026-03-25T10:00:00Z',
- updated_at: '2026-03-25T10:00:00Z',
- ...overrides,
- };
-}
-
-// ── storyStatusLabel ──────────────────────────────────────────────────────────
-
-describe('storyStatusLabel', () => {
- it('returns human-readable label for PENDING', () => {
- assert.equal(storyStatusLabel('PENDING'), 'Pending');
- });
-
- it('returns human-readable label for IN_PROGRESS', () => {
- assert.equal(storyStatusLabel('IN_PROGRESS'), 'In Progress');
- });
-
- it('returns human-readable label for SHIPPABLE', () => {
- assert.equal(storyStatusLabel('SHIPPABLE'), 'Shippable');
- });
-
- it('returns human-readable label for DEPLOYED', () => {
- assert.equal(storyStatusLabel('DEPLOYED'), 'Deployed');
- });
-
- it('returns human-readable label for VALIDATING', () => {
- assert.equal(storyStatusLabel('VALIDATING'), 'Validating');
- });
-
- it('returns human-readable label for REVIEW_READY', () => {
- assert.equal(storyStatusLabel('REVIEW_READY'), 'Review Ready');
- });
-
- it('returns human-readable label for NEEDS_FIX', () => {
- assert.equal(storyStatusLabel('NEEDS_FIX'), 'Needs Fix');
- });
-
- it('falls back to the raw status for unknown values', () => {
- assert.equal(storyStatusLabel('UNKNOWN_STATE'), 'UNKNOWN_STATE');
- });
-});
-
-// ── renderStoryCard ───────────────────────────────────────────────────────────
-
-describe('renderStoryCard', () => {
- let doc;
- beforeEach(() => { doc = makeMockDoc(); });
-
- it('renders the story name', () => {
- const card = renderStoryCard(makeStory(), doc);
- function findText(el, text) {
- if (el.textContent === text) return true;
- return (el.children || []).some(c => findText(c, text));
- }
- assert.ok(findText(card, 'Add login page'), 'card should contain story name');
- });
-
- it('has story-card class', () => {
- const card = renderStoryCard(makeStory(), doc);
- assert.ok(card.className.split(' ').includes('story-card'), 'root element should have story-card class');
- });
-
- it('status badge has data-status matching story status', () => {
- const card = renderStoryCard(makeStory({ status: 'IN_PROGRESS' }), doc);
- const badge = card.querySelector('.story-status-badge');
- assert.ok(badge, 'badge element should exist');
- assert.equal(badge.dataset.status, 'IN_PROGRESS');
- });
-
- it('status badge shows human-readable label', () => {
- const card = renderStoryCard(makeStory({ status: 'REVIEW_READY' }), doc);
- const badge = card.querySelector('.story-status-badge');
- assert.equal(badge.textContent, 'Review Ready');
- });
-
- it('shows project_id', () => {
- const card = renderStoryCard(makeStory({ project_id: 'nav' }), doc);
- function findText(el, text) {
- if (el.textContent === text) return true;
- return (el.children || []).some(c => findText(c, text));
- }
- assert.ok(findText(card, 'nav'), 'card should show project_id');
- });
-
- it('shows branch_name when present', () => {
- const card = renderStoryCard(makeStory({ branch_name: 'story/my-feature' }), doc);
- function findText(el, text) {
- if (el.textContent && el.textContent.includes(text)) return true;
- return (el.children || []).some(c => findText(c, text));
- }
- assert.ok(findText(card, 'story/my-feature'), 'card should show branch_name');
- });
-
- it('does not show branch section when branch_name is empty', () => {
- const card = renderStoryCard(makeStory({ branch_name: '' }), doc);
- const branchEl = card.querySelector('.story-branch');
- assert.ok(!branchEl, 'no .story-branch element when branch is empty');
- });
-
- it('card dataset.storyId is set to story id', () => {
- const card = renderStoryCard(makeStory({ id: 'abc-123' }), doc);
- assert.equal(card.dataset.storyId, 'abc-123');
- });
-});