summaryrefslogtreecommitdiff
path: root/admin-panel/src/client/components/task-modal.ts
diff options
context:
space:
mode:
authorArseney300 <Arseney300@gmail.com>2026-03-05 09:47:09 +0700
committerArseney300 <Arseney300@gmail.com>2026-03-05 09:47:09 +0700
commit0885d20ac18c252f6735cd288448d5d93e95b80f (patch)
treeedfb6814e97d4000605d9f6a918c30c6aca9e10c /admin-panel/src/client/components/task-modal.ts
parent22749b796d7a5e4efa9494156a3fdcfe39028da7 (diff)
WIP: admin_panel: add featurefeature/admin_panel
Diffstat (limited to 'admin-panel/src/client/components/task-modal.ts')
-rw-r--r--admin-panel/src/client/components/task-modal.ts271
1 files changed, 271 insertions, 0 deletions
diff --git a/admin-panel/src/client/components/task-modal.ts b/admin-panel/src/client/components/task-modal.ts
new file mode 100644
index 0000000..725d921
--- /dev/null
+++ b/admin-panel/src/client/components/task-modal.ts
@@ -0,0 +1,271 @@
+import { api, type Task } from '../lib/api.js';
+import { renderMarkdown } from '../lib/markdown.js';
+
+const STATUS_ORDER = ['todo', 'plan', 'plan_review', 'impl', 'impl_review', 'test', 'done'];
+const STATUS_LABELS: Record<string, string> = {
+ todo: 'Req', plan: 'Plan', plan_review: 'Plan Review',
+ impl: 'Impl', impl_review: 'Impl Review', test: 'Test', done: 'Done'
+};
+
+const TABS = ['Requirements', 'Plan', 'Implementation', 'Reviews', 'Tests', 'Agent Log'];
+
+export function showTaskModal(projectName: string, task: Task): void {
+ document.querySelector('.modal-overlay')?.remove();
+
+ const overlay = document.createElement('div');
+ overlay.className = 'modal-overlay';
+ overlay.innerHTML = `
+ <div class="modal task-modal">
+ <div class="modal-header">
+ <div class="top-row">
+ <span class="task-id" style="font-size:14px">#${task.id}</span>
+ <button class="btn-icon close-btn" style="font-size:20px">&times;</button>
+ </div>
+ <h2 style="width:100%">${escapeHtml(task.title)}</h2>
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
+ <span class="badge badge-${task.priority}">${task.priority}</span>
+ <span class="badge badge-level">L${task.level}</span>
+ <span class="badge" style="background:var(--border);color:var(--text-secondary)">
+ ${STATUS_LABELS[task.status] || task.status}
+ </span>
+ ${task.current_agent ? `<span class="badge badge-agent">${escapeHtml(task.current_agent)}</span>` : ''}
+ ${parseTags(task.tags).map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')}
+ </div>
+ </div>
+
+ ${renderLifecycleBar(task)}
+
+ <div class="tabs" id="task-tabs">
+ ${TABS.map((tab, i) => `
+ <button class="tab ${i === 0 ? 'active' : ''}" data-tab="${i}">${tab}</button>
+ `).join('')}
+ </div>
+
+ <div class="tab-content" id="tab-content">
+ ${renderTabContent(0, task)}
+ </div>
+
+ <div class="modal-footer">
+ <div class="pipeline-controls">
+ <button class="btn btn-primary btn-sm" id="run-pipeline-btn" title="Run full pipeline">Run Pipeline</button>
+ <button class="btn btn-secondary btn-sm" id="step-pipeline-btn" title="Run next step">Step</button>
+ <button class="btn btn-danger btn-sm" id="stop-pipeline-btn" title="Stop pipeline">Stop</button>
+ </div>
+ <div style="flex:1"></div>
+ <button class="btn btn-danger btn-sm" id="delete-task-btn">Delete</button>
+ </div>
+ </div>
+ `;
+
+ document.body.appendChild(overlay);
+
+ // Tab switching
+ overlay.querySelectorAll('.tab').forEach(tab => {
+ tab.addEventListener('click', () => {
+ overlay.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
+ tab.classList.add('active');
+ const tabIdx = parseInt((tab as HTMLElement).dataset.tab!);
+ overlay.querySelector('#tab-content')!.innerHTML = renderTabContent(tabIdx, task);
+ });
+ });
+
+ // Close
+ overlay.querySelectorAll('.close-btn').forEach(btn => {
+ btn.addEventListener('click', () => overlay.remove());
+ });
+ overlay.addEventListener('click', (e) => {
+ if (e.target === overlay) overlay.remove();
+ });
+
+ // Pipeline controls
+ overlay.querySelector('#run-pipeline-btn')?.addEventListener('click', async () => {
+ try {
+ await api.runPipeline(projectName, task.id);
+ } catch (err) {
+ alert((err as Error).message);
+ }
+ });
+
+ overlay.querySelector('#step-pipeline-btn')?.addEventListener('click', async () => {
+ try {
+ await api.stepPipeline(projectName, task.id);
+ } catch (err) {
+ alert((err as Error).message);
+ }
+ });
+
+ overlay.querySelector('#stop-pipeline-btn')?.addEventListener('click', async () => {
+ try {
+ await api.stopPipeline(projectName, task.id);
+ } catch (err) {
+ alert((err as Error).message);
+ }
+ });
+
+ // Delete
+ overlay.querySelector('#delete-task-btn')?.addEventListener('click', async () => {
+ if (!confirm(`Delete task #${task.id}?`)) return;
+ try {
+ await api.deleteTask(projectName, task.id);
+ overlay.remove();
+ } catch (err) {
+ alert((err as Error).message);
+ }
+ });
+}
+
+function renderLifecycleBar(task: Task): string {
+ const currentIdx = STATUS_ORDER.indexOf(task.status);
+ return `
+ <div class="lifecycle-bar">
+ ${STATUS_ORDER.map((s, i) => {
+ let cls = 'lifecycle-step';
+ if (i < currentIdx) cls += ' completed';
+ else if (i === currentIdx) cls += ' current';
+ return `<div class="${cls}" title="${STATUS_LABELS[s]}"></div>`;
+ }).join('')}
+ </div>
+ `;
+}
+
+function renderTabContent(tabIdx: number, task: Task): string {
+ switch (tabIdx) {
+ case 0: // Requirements
+ return `
+ <div style="color:var(--text-secondary);font-size:13px">
+ ${task.description
+ ? `<div>${renderMarkdown(task.description)}</div>`
+ : '<p>No description provided.</p>'
+ }
+ ${task.done_when ? `
+ <h4 style="margin-top:16px;color:var(--text-primary)">Done When</h4>
+ <div>${renderMarkdown(task.done_when)}</div>
+ ` : ''}
+ </div>
+ `;
+
+ case 1: // Plan
+ return `
+ <div style="color:var(--text-secondary);font-size:13px">
+ ${task.plan
+ ? `<div>${renderMarkdown(task.plan)}</div>`
+ : '<p>No plan generated yet. Run the pipeline to create one.</p>'
+ }
+ ${task.decision_log ? `
+ <h4 style="margin-top:16px;color:var(--text-primary)">Decision Log</h4>
+ <div>${renderMarkdown(task.decision_log)}</div>
+ ` : ''}
+ </div>
+ `;
+
+ case 2: // Implementation
+ return `
+ <div style="color:var(--text-secondary);font-size:13px">
+ ${task.implementation_notes
+ ? `<div>${renderMarkdown(task.implementation_notes)}</div>`
+ : '<p>No implementation notes yet.</p>'
+ }
+ ${task.branch_name ? `<p style="margin-top:12px">Branch: <code>${escapeHtml(task.branch_name)}</code></p>` : ''}
+ ${task.diff ? `
+ <h4 style="margin-top:16px;color:var(--text-primary)">Diff</h4>
+ <div class="diff-viewer">${renderDiff(task.diff)}</div>
+ ` : ''}
+ </div>
+ `;
+
+ case 3: // Reviews
+ return `
+ <div style="color:var(--text-secondary);font-size:13px">
+ <h4 style="color:var(--text-primary)">Plan Reviews (${task.plan_review_count})</h4>
+ ${renderJsonArray(task.plan_review_comments, 'No plan reviews yet.')}
+
+ <h4 style="margin-top:16px;color:var(--text-primary)">Code Reviews (${task.impl_review_count})</h4>
+ ${renderJsonArray(task.review_comments, 'No code reviews yet.')}
+
+ ${task.gerrit_change_number ? `
+ <p style="margin-top:16px">Gerrit Change: <a href="/gerrit/${task.gerrit_change_number}" style="color:var(--accent-glow)">#${task.gerrit_change_number}</a></p>
+ ` : ''}
+ </div>
+ `;
+
+ case 4: // Tests
+ return `
+ <div style="color:var(--text-secondary);font-size:13px">
+ ${renderJsonArray(task.test_results, 'No test results yet. Run the pipeline to generate tests.')}
+ </div>
+ `;
+
+ case 5: // Agent Log
+ return `
+ <div class="agent-log">
+ ${renderAgentLog(task.agent_log)}
+ </div>
+ `;
+
+ default:
+ return '';
+ }
+}
+
+function renderDiff(diff: string): string {
+ return diff.split('\n').map(line => {
+ let cls = 'diff-line';
+ if (line.startsWith('+')) cls += ' diff-add';
+ else if (line.startsWith('-')) cls += ' diff-del';
+ else if (line.startsWith('@@')) cls += ' diff-hunk';
+ return `<div class="${cls}">${escapeHtml(line)}</div>`;
+ }).join('');
+}
+
+function renderJsonArray(jsonStr: string, emptyMsg: string): string {
+ try {
+ const items = JSON.parse(jsonStr);
+ if (!Array.isArray(items) || items.length === 0) return `<p>${emptyMsg}</p>`;
+ return items.map((item: unknown) => {
+ if (typeof item === 'string') return `<div class="log-entry"><div class="log-message">${escapeHtml(item)}</div></div>`;
+ if (typeof item === 'object' && item !== null) {
+ const obj = item as Record<string, unknown>;
+ return `<div class="log-entry">
+ ${obj.agent ? `<div class="log-agent">${escapeHtml(String(obj.agent))}</div>` : ''}
+ <div class="log-message">${escapeHtml(String(obj.message || obj.comment || JSON.stringify(obj)))}</div>
+ </div>`;
+ }
+ return '';
+ }).join('');
+ } catch {
+ return `<p>${emptyMsg}</p>`;
+ }
+}
+
+function renderAgentLog(logStr: string): string {
+ try {
+ const entries = JSON.parse(logStr);
+ if (!Array.isArray(entries) || entries.length === 0) {
+ return '<div class="empty-state"><p>No agent activity yet.</p></div>';
+ }
+ return entries.map((entry: Record<string, unknown>) => `
+ <div class="log-entry">
+ <span class="log-time">${entry.timestamp ? new Date(String(entry.timestamp)).toLocaleTimeString() : ''}</span>
+ <span class="log-agent">${escapeHtml(String(entry.agent || ''))}</span>
+ <span class="log-message">${escapeHtml(String(entry.message || ''))}</span>
+ </div>
+ `).join('');
+ } catch {
+ return '<div class="empty-state"><p>No agent activity yet.</p></div>';
+ }
+}
+
+function parseTags(tagsStr: string): string[] {
+ try {
+ const parsed = JSON.parse(tagsStr);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch {
+ return [];
+ }
+}
+
+function escapeHtml(text: string): string {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}