ai-control-center 1.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +584 -0
- package/bin/aicc.js +772 -0
- package/lib/actions/approve.js +71 -0
- package/lib/actions/assign-project.js +132 -0
- package/lib/actions/browser-test.js +64 -0
- package/lib/actions/cleanup.js +174 -0
- package/lib/actions/debug.js +298 -0
- package/lib/actions/deploy.js +1229 -0
- package/lib/actions/fix-bug.js +134 -0
- package/lib/actions/new-feature.js +255 -0
- package/lib/actions/reject.js +307 -0
- package/lib/actions/review.js +706 -0
- package/lib/actions/status.js +47 -0
- package/lib/agents/browser-qa-agent.js +611 -0
- package/lib/agents/payment-agent.js +116 -0
- package/lib/agents/suggestion-agent.js +88 -0
- package/lib/cli.js +303 -0
- package/lib/config.js +243 -0
- package/lib/hub/hub-server.js +440 -0
- package/lib/hub/project-poller.js +75 -0
- package/lib/hub/skill-registry.js +89 -0
- package/lib/hub/state-aggregator.js +204 -0
- package/lib/index.js +471 -0
- package/lib/init/doctor.js +523 -0
- package/lib/init/presets.js +222 -0
- package/lib/init/skill-fetcher.js +77 -0
- package/lib/init/wizard.js +973 -0
- package/lib/integrations/codex-runner.js +128 -0
- package/lib/integrations/github-actions.js +248 -0
- package/lib/integrations/github-reporter.js +229 -0
- package/lib/integrations/screenshot-store.js +102 -0
- package/lib/openclaw/bridge.js +650 -0
- package/lib/openclaw/generate-skill.js +235 -0
- package/lib/openclaw/openclaw.json +64 -0
- package/lib/orchestrator/autonomous-loop.js +429 -0
- package/lib/orchestrator/thread-triggers.js +63 -0
- package/lib/roleplay/agent-messenger.js +75 -0
- package/lib/roleplay/discussion-threads.js +303 -0
- package/lib/roleplay/health-monitor.js +121 -0
- package/lib/roleplay/pm-agent.js +513 -0
- package/lib/roleplay/roleplay-config.js +25 -0
- package/lib/roleplay/room.js +164 -0
- package/lib/shared/action-runner.js +2330 -0
- package/lib/shared/event-bus.js +185 -0
- package/lib/slack/bot.js +378 -0
- package/lib/telegram/bot.js +416 -0
- package/lib/telegram/commands.js +1267 -0
- package/lib/telegram/keyboards.js +113 -0
- package/lib/telegram/notifications.js +247 -0
- package/lib/twitch/bot.js +354 -0
- package/lib/twitch/commands.js +302 -0
- package/lib/twitch/notifications.js +63 -0
- package/lib/utils/achievements.js +191 -0
- package/lib/utils/activity-log.js +182 -0
- package/lib/utils/agent-leaderboard.js +119 -0
- package/lib/utils/audit-logger.js +232 -0
- package/lib/utils/codebase-context.js +288 -0
- package/lib/utils/codebase-indexer.js +381 -0
- package/lib/utils/config-schema.js +230 -0
- package/lib/utils/context-compressor.js +172 -0
- package/lib/utils/correlation.js +63 -0
- package/lib/utils/cost-tracker.js +423 -0
- package/lib/utils/cron-scheduler.js +53 -0
- package/lib/utils/db-adapter.js +293 -0
- package/lib/utils/display.js +272 -0
- package/lib/utils/errors.js +116 -0
- package/lib/utils/format.js +134 -0
- package/lib/utils/intent-engine.js +464 -0
- package/lib/utils/mcp-client.js +238 -0
- package/lib/utils/model-ab-test.js +164 -0
- package/lib/utils/notify.js +122 -0
- package/lib/utils/persona-loader.js +80 -0
- package/lib/utils/pipeline-lock.js +73 -0
- package/lib/utils/pipeline.js +214 -0
- package/lib/utils/plugin-runner.js +234 -0
- package/lib/utils/rate-limiter.js +84 -0
- package/lib/utils/rbac.js +74 -0
- package/lib/utils/runner.js +1809 -0
- package/lib/utils/security.js +191 -0
- package/lib/utils/self-healer.js +144 -0
- package/lib/utils/skill-loader.js +255 -0
- package/lib/utils/spinner.js +132 -0
- package/lib/utils/stage-queue.js +50 -0
- package/lib/utils/state-machine.js +89 -0
- package/lib/utils/status-bar.js +327 -0
- package/lib/utils/token-estimator.js +101 -0
- package/lib/utils/ux-analyzer.js +101 -0
- package/lib/utils/webhook-emitter.js +83 -0
- package/lib/web/public/css/styles.css +417 -0
- package/lib/web/public/dark-mode.js +44 -0
- package/lib/web/public/hub/kanban.html +206 -0
- package/lib/web/public/index.html +45 -0
- package/lib/web/public/js/app.js +71 -0
- package/lib/web/public/js/ask.js +110 -0
- package/lib/web/public/js/dashboard.js +165 -0
- package/lib/web/public/js/deploy.js +72 -0
- package/lib/web/public/js/feature.js +79 -0
- package/lib/web/public/js/health.js +65 -0
- package/lib/web/public/js/logs.js +93 -0
- package/lib/web/public/js/review.js +123 -0
- package/lib/web/public/js/ws-client.js +82 -0
- package/lib/web/public/office/css/office.css +678 -0
- package/lib/web/public/office/index.html +148 -0
- package/lib/web/public/office/js/achievements-ui.js +117 -0
- package/lib/web/public/office/js/character.js +1056 -0
- package/lib/web/public/office/js/chat-bubbles.js +177 -0
- package/lib/web/public/office/js/cost-overlay.js +123 -0
- package/lib/web/public/office/js/day-night.js +68 -0
- package/lib/web/public/office/js/effects.js +632 -0
- package/lib/web/public/office/js/engine.js +146 -0
- package/lib/web/public/office/js/feature-ticket.js +216 -0
- package/lib/web/public/office/js/hub-client.js +60 -0
- package/lib/web/public/office/js/main.js +1757 -0
- package/lib/web/public/office/js/office-layout.js +1524 -0
- package/lib/web/public/office/js/pathfinding.js +144 -0
- package/lib/web/public/office/js/pixel-sprites.js +1454 -0
- package/lib/web/public/office/js/progress-bars.js +117 -0
- package/lib/web/public/office/js/replay.js +191 -0
- package/lib/web/public/office/js/sound-effects.js +91 -0
- package/lib/web/public/office/js/sprite-renderer.js +211 -0
- package/lib/web/public/office/js/stamina-system.js +89 -0
- package/lib/web/public/office/js/ui.js +107 -0
- package/lib/web/public/onboarding/index.html +243 -0
- package/lib/web/public/timeline/index.html +195 -0
- package/lib/web/routes/api.js +499 -0
- package/lib/web/routes/logs.js +20 -0
- package/lib/web/routes/metrics.js +99 -0
- package/lib/web/server.js +183 -0
- package/lib/web/ws/handler.js +65 -0
- package/package.json +67 -0
- package/templates/agent-architect.md +69 -0
- package/templates/agent-gemini-pm.md +49 -0
- package/templates/agent-gemini-reviewer.md +52 -0
- package/templates/copilot-instructions.md +36 -0
- package/templates/pipelines/mobile.json +27 -0
- package/templates/pipelines/nodejs-api.json +27 -0
- package/templates/pipelines/python.json +27 -0
- package/templates/pipelines/react.json +27 -0
- package/templates/pipelines/salesforce.json +27 -0
- package/templates/role-gemini.md +97 -0
- package/templates/skill-architect.md +114 -0
- package/templates/skill-browser-qa.md +50 -0
- package/templates/skill-bug-from-qa.md +58 -0
- package/templates/skill-chatbot.md +93 -0
- package/templates/skill-implement.md +78 -0
- package/templates/skill-openclaw.md +174 -0
- package/templates/skill-payment.md +110 -0
- package/templates/skill-pm-spec.md +77 -0
- package/templates/skill-requirement-capture.md +97 -0
- package/templates/skill-review.md +108 -0
- package/templates/skill-reviewer-qa.md +44 -0
- package/templates/skill-suggestion.md +45 -0
- package/templates/skill-template.md +142 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* New Feature / Bug Fix view.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function renderFeature(container) {
|
|
6
|
+
container.innerHTML = `
|
|
7
|
+
<div class="card">
|
|
8
|
+
<div class="card-title">Submit New Feature / Bug Fix</div>
|
|
9
|
+
|
|
10
|
+
<div class="form-group">
|
|
11
|
+
<label>Type</label>
|
|
12
|
+
<select id="feat-type">
|
|
13
|
+
<option value="feature">Feature</option>
|
|
14
|
+
<option value="bug">Bug Fix</option>
|
|
15
|
+
</select>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="form-group">
|
|
19
|
+
<label>Description (min 10 characters)</label>
|
|
20
|
+
<textarea id="feat-desc" placeholder="e.g. Add MYOB bank transaction inbound sync support" rows="4"></textarea>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="form-group">
|
|
24
|
+
<label>Pipeline Mode</label>
|
|
25
|
+
<select id="feat-mode">
|
|
26
|
+
<option value="auto">Auto — run full pipeline, notify when done</option>
|
|
27
|
+
<option value="manual">Manual — step through each stage</option>
|
|
28
|
+
</select>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="btn-group">
|
|
32
|
+
<button class="btn btn-primary" id="feat-submit">Submit Feature</button>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div id="feat-result" style="margin-top: 16px; font-family: var(--mono); font-size: 13px;"></div>
|
|
36
|
+
</div>
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
document.getElementById('feat-submit').addEventListener('click', submitFeature);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function submitFeature() {
|
|
43
|
+
const btn = document.getElementById('feat-submit');
|
|
44
|
+
const desc = document.getElementById('feat-desc').value.trim();
|
|
45
|
+
const mode = document.getElementById('feat-mode').value;
|
|
46
|
+
const type = document.getElementById('feat-type').value;
|
|
47
|
+
const result = document.getElementById('feat-result');
|
|
48
|
+
|
|
49
|
+
if (desc.length < 10) {
|
|
50
|
+
result.innerHTML = '<span style="color: var(--red);">Description must be at least 10 characters.</span>';
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
btn.disabled = true;
|
|
55
|
+
btn.innerHTML = '<span class="spinner"></span> Submitting...';
|
|
56
|
+
result.innerHTML = '<span style="color: var(--text-dim);">Creating feature inbox file...</span>';
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch('/api/feature', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ description: desc, mode, type }),
|
|
63
|
+
});
|
|
64
|
+
const data = await res.json();
|
|
65
|
+
|
|
66
|
+
if (data.success) {
|
|
67
|
+
result.innerHTML = `<span style="color: var(--green);">Feature created: ${data.featureId}</span>`;
|
|
68
|
+
window.showToast('Feature submitted!', 'success');
|
|
69
|
+
document.getElementById('feat-desc').value = '';
|
|
70
|
+
} else {
|
|
71
|
+
result.innerHTML = `<span style="color: var(--red);">${data.error}</span>`;
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
result.innerHTML = `<span style="color: var(--red);">Network error: ${err.message}</span>`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
btn.disabled = false;
|
|
78
|
+
btn.textContent = 'Submit Feature';
|
|
79
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health check view — shows AI tool + org availability.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function renderHealth(container) {
|
|
6
|
+
container.innerHTML = `
|
|
7
|
+
<div class="card">
|
|
8
|
+
<div class="card-title">System Health Check</div>
|
|
9
|
+
<div id="health-content">
|
|
10
|
+
<div style="color: var(--text-dim);"><span class="spinner"></span> Checking...</div>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
loadHealth();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function loadHealth() {
|
|
19
|
+
const el = document.getElementById('health-content');
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const data = await fetch('/api/health').then(r => r.json());
|
|
23
|
+
|
|
24
|
+
const row = (icon, label, detail) =>
|
|
25
|
+
`<div class="health-row">
|
|
26
|
+
<span class="health-icon ${icon ? 'health-ok' : 'health-fail'}">${icon ? '✓' : '✗'}</span>
|
|
27
|
+
<span class="health-label">${label}</span>
|
|
28
|
+
<span class="health-detail">${detail}</span>
|
|
29
|
+
</div>`;
|
|
30
|
+
|
|
31
|
+
let html = '';
|
|
32
|
+
|
|
33
|
+
// AI Tools
|
|
34
|
+
html += '<div style="color: var(--text-dim); font-size: 11px; margin: 8px 0 4px;">AI Tools</div>';
|
|
35
|
+
html += row(data.gemini?.available, 'gemini', data.gemini?.path || 'not found');
|
|
36
|
+
html += row(data.claude?.available, 'claude', data.claude?.path || 'not found');
|
|
37
|
+
html += row(data.copilot?.available, 'copilot', data.copilot?.path || 'not found');
|
|
38
|
+
html += row(data.gh?.available, 'gh', data.gh?.path || 'not found');
|
|
39
|
+
|
|
40
|
+
// Platform CLI
|
|
41
|
+
html += '<div style="color: var(--text-dim); font-size: 11px; margin: 12px 0 4px;">Platform CLI</div>';
|
|
42
|
+
html += row(data.sf?.available, 'sf', data.sf?.path || 'not found');
|
|
43
|
+
|
|
44
|
+
// Workflow
|
|
45
|
+
html += '<div style="color: var(--text-dim); font-size: 11px; margin: 12px 0 4px;">Workflow</div>';
|
|
46
|
+
const wf = data.workflow || {};
|
|
47
|
+
html += row(wf.aiWorkflow, '.ai-workflow/', wf.aiWorkflow ? 'exists' : 'missing');
|
|
48
|
+
html += row(wf.skills, '.claude/skills/', wf.skills ? 'exists' : 'missing');
|
|
49
|
+
html += row(wf.agents, '.claude/agents/', wf.agents ? 'exists' : 'missing');
|
|
50
|
+
|
|
51
|
+
// Pipeline
|
|
52
|
+
const p = data.pipeline || {};
|
|
53
|
+
html += '<div style="color: var(--text-dim); font-size: 11px; margin: 12px 0 4px;">Pipeline</div>';
|
|
54
|
+
html += `<div class="health-row">
|
|
55
|
+
<span class="health-icon" style="color: var(--cyan);">◆</span>
|
|
56
|
+
<span class="health-label">stage</span>
|
|
57
|
+
<span class="health-detail">${p.stage || 'idle'} · ${p.current_feature || 'none'}</span>
|
|
58
|
+
</div>`;
|
|
59
|
+
|
|
60
|
+
el.innerHTML = html;
|
|
61
|
+
|
|
62
|
+
} catch (err) {
|
|
63
|
+
el.innerHTML = `<span style="color: var(--red);">Health check failed: ${err.message}</span>`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logs view — session log viewer with agent filter.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
let currentFilter = 'all';
|
|
6
|
+
|
|
7
|
+
export function renderLogs(container) {
|
|
8
|
+
container.innerHTML = `
|
|
9
|
+
<div class="card">
|
|
10
|
+
<div class="card-title">Session Logs</div>
|
|
11
|
+
|
|
12
|
+
<div class="filter-tabs" id="log-filters">
|
|
13
|
+
<button class="filter-tab active" data-filter="all">All</button>
|
|
14
|
+
<button class="filter-tab" data-filter="GEMINI" style="color: var(--cyan);">Gemini</button>
|
|
15
|
+
<button class="filter-tab" data-filter="CLAUDE" style="color: var(--magenta);">Claude</button>
|
|
16
|
+
<button class="filter-tab" data-filter="COPILOT" style="color: var(--blue);">Copilot</button>
|
|
17
|
+
<button class="filter-tab" data-filter="PIPELINE" style="color: var(--yellow);">Pipeline</button>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div id="log-content" class="log-viewer">Loading...</div>
|
|
21
|
+
|
|
22
|
+
<div style="margin-top: 8px; display: flex; justify-content: space-between; align-items: center;">
|
|
23
|
+
<span id="log-count" class="dim" style="font-size: 11px;"></span>
|
|
24
|
+
<button class="btn" id="log-refresh" style="font-size: 12px;">Refresh</button>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
// Filter tab handlers
|
|
30
|
+
document.getElementById('log-filters').addEventListener('click', (e) => {
|
|
31
|
+
if (!e.target.dataset.filter) return;
|
|
32
|
+
currentFilter = e.target.dataset.filter;
|
|
33
|
+
document.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active'));
|
|
34
|
+
e.target.classList.add('active');
|
|
35
|
+
loadLogs();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
document.getElementById('log-refresh').addEventListener('click', loadLogs);
|
|
39
|
+
|
|
40
|
+
// Listen for live log events
|
|
41
|
+
window.addEventListener('xconn', onLogUpdate);
|
|
42
|
+
|
|
43
|
+
loadLogs();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function loadLogs() {
|
|
47
|
+
const contentEl = document.getElementById('log-content');
|
|
48
|
+
const countEl = document.getElementById('log-count');
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const data = await fetch('/api/logs?lines=200').then(r => r.json());
|
|
52
|
+
const lines = data.content.split('\n').filter(l => l.trim());
|
|
53
|
+
|
|
54
|
+
const filtered = currentFilter === 'all'
|
|
55
|
+
? lines
|
|
56
|
+
: lines.filter(l => l.includes(`[${currentFilter}`));
|
|
57
|
+
|
|
58
|
+
contentEl.innerHTML = filtered.map(colorLine).join('\n');
|
|
59
|
+
countEl.textContent = `${filtered.length} line(s)${data.total ? ` of ${data.total} total` : ''}`;
|
|
60
|
+
|
|
61
|
+
// Auto-scroll to bottom
|
|
62
|
+
contentEl.scrollTop = contentEl.scrollHeight;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
contentEl.textContent = 'Failed to load logs.';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function colorLine(line) {
|
|
69
|
+
let cls = '';
|
|
70
|
+
if (line.includes('[GEMINI')) cls = 'log-gemini';
|
|
71
|
+
else if (line.includes('[CLAUDE')) cls = 'log-claude';
|
|
72
|
+
else if (line.includes('[COPILOT')) cls = 'log-copilot';
|
|
73
|
+
else if (line.includes('[PIPELINE')) cls = 'log-pipeline';
|
|
74
|
+
else if (line.includes('[ERROR')) cls = 'log-error';
|
|
75
|
+
else if (line.includes('[SUCCESS')) cls = 'log-success';
|
|
76
|
+
|
|
77
|
+
const escaped = line.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
78
|
+
return `<span class="log-line ${cls}">${escaped}</span>`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function onLogUpdate(e) {
|
|
82
|
+
if (e.detail.type !== 'log') return;
|
|
83
|
+
const { agent, message } = e.detail.data;
|
|
84
|
+
|
|
85
|
+
if (currentFilter !== 'all' && !agent.includes(currentFilter)) return;
|
|
86
|
+
|
|
87
|
+
const contentEl = document.getElementById('log-content');
|
|
88
|
+
if (!contentEl) return;
|
|
89
|
+
|
|
90
|
+
const line = `[${new Date().toLocaleTimeString()}] [${agent}] ${message}`;
|
|
91
|
+
contentEl.innerHTML += '\n' + colorLine(line);
|
|
92
|
+
contentEl.scrollTop = contentEl.scrollHeight;
|
|
93
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review view — verdict display + approve/reject.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function renderReview(container) {
|
|
6
|
+
container.innerHTML = `
|
|
7
|
+
<div class="card">
|
|
8
|
+
<div class="card-title">Latest Review</div>
|
|
9
|
+
<div id="review-content" style="font-family: var(--mono); font-size: 13px;">Loading...</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="card" id="review-actions-card" style="display: none;">
|
|
13
|
+
<div class="card-title">Actions</div>
|
|
14
|
+
<div class="btn-group">
|
|
15
|
+
<button class="btn btn-success" id="review-approve">Approve</button>
|
|
16
|
+
<button class="btn btn-danger" id="review-reject">Reject</button>
|
|
17
|
+
</div>
|
|
18
|
+
<div id="reject-reason-wrap" style="display: none; margin-top: 12px;">
|
|
19
|
+
<div class="form-group">
|
|
20
|
+
<label>Rejection Reason</label>
|
|
21
|
+
<textarea id="reject-reason" placeholder="Describe what needs to be fixed..." rows="3"></textarea>
|
|
22
|
+
</div>
|
|
23
|
+
<button class="btn btn-danger" id="reject-confirm">Confirm Reject</button>
|
|
24
|
+
</div>
|
|
25
|
+
<div id="action-result" style="margin-top: 12px; font-family: var(--mono); font-size: 13px;"></div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="card">
|
|
29
|
+
<div class="card-title">Review File Content</div>
|
|
30
|
+
<div id="review-file" class="log-viewer" style="max-height: 600px;">Loading...</div>
|
|
31
|
+
</div>
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
loadReview();
|
|
35
|
+
|
|
36
|
+
document.getElementById('review-approve').addEventListener('click', approveFeature);
|
|
37
|
+
document.getElementById('review-reject').addEventListener('click', () => {
|
|
38
|
+
document.getElementById('reject-reason-wrap').style.display = 'block';
|
|
39
|
+
});
|
|
40
|
+
document.getElementById('reject-confirm').addEventListener('click', rejectFeature);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function loadReview() {
|
|
44
|
+
const contentEl = document.getElementById('review-content');
|
|
45
|
+
const fileEl = document.getElementById('review-file');
|
|
46
|
+
const actionsCard = document.getElementById('review-actions-card');
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const review = await fetch('/api/review/latest').then(r => r.json());
|
|
50
|
+
|
|
51
|
+
if (!review.found) {
|
|
52
|
+
contentEl.textContent = 'No reviews available yet.';
|
|
53
|
+
fileEl.textContent = 'Run a code review first.';
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Verdict panel
|
|
58
|
+
const isApproved = review.verdict === 'APPROVED';
|
|
59
|
+
const verdictClass = isApproved ? 'verdict-approved' : 'verdict-rejected';
|
|
60
|
+
const color = isApproved ? 'var(--green)' : 'var(--red)';
|
|
61
|
+
|
|
62
|
+
contentEl.innerHTML = `
|
|
63
|
+
<div class="verdict-panel ${verdictClass}">
|
|
64
|
+
<div class="verdict-title" style="color: ${color};">${review.verdict}</div>
|
|
65
|
+
<div style="color: var(--text-dim); font-size: 12px;">${review.name}</div>
|
|
66
|
+
</div>
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
// Show actions if review is complete
|
|
70
|
+
const status = await fetch('/api/status').then(r => r.json());
|
|
71
|
+
if (status.stage === 'review_complete') {
|
|
72
|
+
actionsCard.style.display = 'block';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Full review content
|
|
76
|
+
fileEl.textContent = review.content;
|
|
77
|
+
|
|
78
|
+
} catch (err) {
|
|
79
|
+
contentEl.textContent = 'Failed to load review.';
|
|
80
|
+
fileEl.textContent = err.message;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function approveFeature() {
|
|
85
|
+
const resultEl = document.getElementById('action-result');
|
|
86
|
+
resultEl.innerHTML = '<span class="spinner"></span> Approving...';
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch('/api/approve', { method: 'POST' });
|
|
90
|
+
const data = await res.json();
|
|
91
|
+
if (data.success) {
|
|
92
|
+
resultEl.innerHTML = '<span style="color: var(--green);">Feature approved!</span>';
|
|
93
|
+
window.showToast('Feature approved!', 'success');
|
|
94
|
+
} else {
|
|
95
|
+
resultEl.innerHTML = `<span style="color: var(--red);">${data.error}</span>`;
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
resultEl.innerHTML = `<span style="color: var(--red);">${err.message}</span>`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function rejectFeature() {
|
|
103
|
+
const reason = document.getElementById('reject-reason').value.trim() || 'Rejected via web dashboard';
|
|
104
|
+
const resultEl = document.getElementById('action-result');
|
|
105
|
+
resultEl.innerHTML = '<span class="spinner"></span> Rejecting...';
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetch('/api/reject', {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
111
|
+
body: JSON.stringify({ reason }),
|
|
112
|
+
});
|
|
113
|
+
const data = await res.json();
|
|
114
|
+
if (data.success) {
|
|
115
|
+
resultEl.innerHTML = '<span style="color: var(--yellow);">Feature rejected. Dispatching fixes to Copilot.</span>';
|
|
116
|
+
window.showToast('Feature rejected', 'error');
|
|
117
|
+
} else {
|
|
118
|
+
resultEl.innerHTML = `<span style="color: var(--red);">${data.error}</span>`;
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
resultEl.innerHTML = `<span style="color: var(--red);">${err.message}</span>`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client with auto-reconnect.
|
|
3
|
+
* Dispatches custom events on window for view modules to listen to.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let ws = null;
|
|
7
|
+
let reconnectTimer = null;
|
|
8
|
+
const RECONNECT_DELAY = 2000;
|
|
9
|
+
|
|
10
|
+
function connect() {
|
|
11
|
+
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
12
|
+
ws = new WebSocket(`${protocol}://${location.host}/ws`);
|
|
13
|
+
|
|
14
|
+
ws.onopen = () => {
|
|
15
|
+
updateIndicator(true);
|
|
16
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
ws.onclose = () => {
|
|
20
|
+
updateIndicator(false);
|
|
21
|
+
reconnectTimer = setTimeout(connect, RECONNECT_DELAY);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
ws.onerror = () => ws.close();
|
|
25
|
+
|
|
26
|
+
ws.onmessage = (e) => {
|
|
27
|
+
try {
|
|
28
|
+
const msg = JSON.parse(e.data);
|
|
29
|
+
window.dispatchEvent(new CustomEvent('xconn', { detail: msg }));
|
|
30
|
+
|
|
31
|
+
// Update header badge on status changes
|
|
32
|
+
if (msg.type === 'status' && msg.data?.status) {
|
|
33
|
+
updateHeaderStatus(msg.data.status);
|
|
34
|
+
}
|
|
35
|
+
if (msg.type === 'clients') {
|
|
36
|
+
const el = document.getElementById('footer-clients');
|
|
37
|
+
if (el) el.textContent = `${msg.data.count} client(s)`;
|
|
38
|
+
}
|
|
39
|
+
} catch { /* ignore non-JSON */ }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function updateIndicator(connected) {
|
|
44
|
+
const dot = document.getElementById('ws-indicator');
|
|
45
|
+
if (!dot) return;
|
|
46
|
+
dot.className = connected ? 'ws-dot ws-connected' : 'ws-dot ws-disconnected';
|
|
47
|
+
dot.title = connected ? 'Connected' : 'Disconnected';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function updateHeaderStatus(status) {
|
|
51
|
+
const stageEl = document.getElementById('header-stage');
|
|
52
|
+
const featEl = document.getElementById('header-feature');
|
|
53
|
+
if (stageEl) {
|
|
54
|
+
const stage = status.stage || 'idle';
|
|
55
|
+
stageEl.textContent = stage;
|
|
56
|
+
stageEl.className = 'badge ' + getStageBadgeClass(stage);
|
|
57
|
+
}
|
|
58
|
+
if (featEl) {
|
|
59
|
+
featEl.textContent = status.current_feature || '';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getStageBadgeClass(stage) {
|
|
64
|
+
if (stage === 'idle') return 'badge-idle';
|
|
65
|
+
if (stage === 'approved') return 'badge-approved';
|
|
66
|
+
if (stage === 'rejected') return 'badge-rejected';
|
|
67
|
+
if (stage === 'deployed') return 'badge-deployed';
|
|
68
|
+
if (stage === 'review_complete') return 'badge-review';
|
|
69
|
+
return 'badge-progress';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Export for other modules
|
|
73
|
+
window.xconnWs = { connect };
|
|
74
|
+
|
|
75
|
+
// Auto-connect on load
|
|
76
|
+
connect();
|
|
77
|
+
|
|
78
|
+
// Fetch initial status for header
|
|
79
|
+
fetch('/api/status')
|
|
80
|
+
.then(r => r.json())
|
|
81
|
+
.then(updateHeaderStatus)
|
|
82
|
+
.catch(() => {});
|