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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +584 -0
  3. package/bin/aicc.js +772 -0
  4. package/lib/actions/approve.js +71 -0
  5. package/lib/actions/assign-project.js +132 -0
  6. package/lib/actions/browser-test.js +64 -0
  7. package/lib/actions/cleanup.js +174 -0
  8. package/lib/actions/debug.js +298 -0
  9. package/lib/actions/deploy.js +1229 -0
  10. package/lib/actions/fix-bug.js +134 -0
  11. package/lib/actions/new-feature.js +255 -0
  12. package/lib/actions/reject.js +307 -0
  13. package/lib/actions/review.js +706 -0
  14. package/lib/actions/status.js +47 -0
  15. package/lib/agents/browser-qa-agent.js +611 -0
  16. package/lib/agents/payment-agent.js +116 -0
  17. package/lib/agents/suggestion-agent.js +88 -0
  18. package/lib/cli.js +303 -0
  19. package/lib/config.js +243 -0
  20. package/lib/hub/hub-server.js +440 -0
  21. package/lib/hub/project-poller.js +75 -0
  22. package/lib/hub/skill-registry.js +89 -0
  23. package/lib/hub/state-aggregator.js +204 -0
  24. package/lib/index.js +471 -0
  25. package/lib/init/doctor.js +523 -0
  26. package/lib/init/presets.js +222 -0
  27. package/lib/init/skill-fetcher.js +77 -0
  28. package/lib/init/wizard.js +973 -0
  29. package/lib/integrations/codex-runner.js +128 -0
  30. package/lib/integrations/github-actions.js +248 -0
  31. package/lib/integrations/github-reporter.js +229 -0
  32. package/lib/integrations/screenshot-store.js +102 -0
  33. package/lib/openclaw/bridge.js +650 -0
  34. package/lib/openclaw/generate-skill.js +235 -0
  35. package/lib/openclaw/openclaw.json +64 -0
  36. package/lib/orchestrator/autonomous-loop.js +429 -0
  37. package/lib/orchestrator/thread-triggers.js +63 -0
  38. package/lib/roleplay/agent-messenger.js +75 -0
  39. package/lib/roleplay/discussion-threads.js +303 -0
  40. package/lib/roleplay/health-monitor.js +121 -0
  41. package/lib/roleplay/pm-agent.js +513 -0
  42. package/lib/roleplay/roleplay-config.js +25 -0
  43. package/lib/roleplay/room.js +164 -0
  44. package/lib/shared/action-runner.js +2330 -0
  45. package/lib/shared/event-bus.js +185 -0
  46. package/lib/slack/bot.js +378 -0
  47. package/lib/telegram/bot.js +416 -0
  48. package/lib/telegram/commands.js +1267 -0
  49. package/lib/telegram/keyboards.js +113 -0
  50. package/lib/telegram/notifications.js +247 -0
  51. package/lib/twitch/bot.js +354 -0
  52. package/lib/twitch/commands.js +302 -0
  53. package/lib/twitch/notifications.js +63 -0
  54. package/lib/utils/achievements.js +191 -0
  55. package/lib/utils/activity-log.js +182 -0
  56. package/lib/utils/agent-leaderboard.js +119 -0
  57. package/lib/utils/audit-logger.js +232 -0
  58. package/lib/utils/codebase-context.js +288 -0
  59. package/lib/utils/codebase-indexer.js +381 -0
  60. package/lib/utils/config-schema.js +230 -0
  61. package/lib/utils/context-compressor.js +172 -0
  62. package/lib/utils/correlation.js +63 -0
  63. package/lib/utils/cost-tracker.js +423 -0
  64. package/lib/utils/cron-scheduler.js +53 -0
  65. package/lib/utils/db-adapter.js +293 -0
  66. package/lib/utils/display.js +272 -0
  67. package/lib/utils/errors.js +116 -0
  68. package/lib/utils/format.js +134 -0
  69. package/lib/utils/intent-engine.js +464 -0
  70. package/lib/utils/mcp-client.js +238 -0
  71. package/lib/utils/model-ab-test.js +164 -0
  72. package/lib/utils/notify.js +122 -0
  73. package/lib/utils/persona-loader.js +80 -0
  74. package/lib/utils/pipeline-lock.js +73 -0
  75. package/lib/utils/pipeline.js +214 -0
  76. package/lib/utils/plugin-runner.js +234 -0
  77. package/lib/utils/rate-limiter.js +84 -0
  78. package/lib/utils/rbac.js +74 -0
  79. package/lib/utils/runner.js +1809 -0
  80. package/lib/utils/security.js +191 -0
  81. package/lib/utils/self-healer.js +144 -0
  82. package/lib/utils/skill-loader.js +255 -0
  83. package/lib/utils/spinner.js +132 -0
  84. package/lib/utils/stage-queue.js +50 -0
  85. package/lib/utils/state-machine.js +89 -0
  86. package/lib/utils/status-bar.js +327 -0
  87. package/lib/utils/token-estimator.js +101 -0
  88. package/lib/utils/ux-analyzer.js +101 -0
  89. package/lib/utils/webhook-emitter.js +83 -0
  90. package/lib/web/public/css/styles.css +417 -0
  91. package/lib/web/public/dark-mode.js +44 -0
  92. package/lib/web/public/hub/kanban.html +206 -0
  93. package/lib/web/public/index.html +45 -0
  94. package/lib/web/public/js/app.js +71 -0
  95. package/lib/web/public/js/ask.js +110 -0
  96. package/lib/web/public/js/dashboard.js +165 -0
  97. package/lib/web/public/js/deploy.js +72 -0
  98. package/lib/web/public/js/feature.js +79 -0
  99. package/lib/web/public/js/health.js +65 -0
  100. package/lib/web/public/js/logs.js +93 -0
  101. package/lib/web/public/js/review.js +123 -0
  102. package/lib/web/public/js/ws-client.js +82 -0
  103. package/lib/web/public/office/css/office.css +678 -0
  104. package/lib/web/public/office/index.html +148 -0
  105. package/lib/web/public/office/js/achievements-ui.js +117 -0
  106. package/lib/web/public/office/js/character.js +1056 -0
  107. package/lib/web/public/office/js/chat-bubbles.js +177 -0
  108. package/lib/web/public/office/js/cost-overlay.js +123 -0
  109. package/lib/web/public/office/js/day-night.js +68 -0
  110. package/lib/web/public/office/js/effects.js +632 -0
  111. package/lib/web/public/office/js/engine.js +146 -0
  112. package/lib/web/public/office/js/feature-ticket.js +216 -0
  113. package/lib/web/public/office/js/hub-client.js +60 -0
  114. package/lib/web/public/office/js/main.js +1757 -0
  115. package/lib/web/public/office/js/office-layout.js +1524 -0
  116. package/lib/web/public/office/js/pathfinding.js +144 -0
  117. package/lib/web/public/office/js/pixel-sprites.js +1454 -0
  118. package/lib/web/public/office/js/progress-bars.js +117 -0
  119. package/lib/web/public/office/js/replay.js +191 -0
  120. package/lib/web/public/office/js/sound-effects.js +91 -0
  121. package/lib/web/public/office/js/sprite-renderer.js +211 -0
  122. package/lib/web/public/office/js/stamina-system.js +89 -0
  123. package/lib/web/public/office/js/ui.js +107 -0
  124. package/lib/web/public/onboarding/index.html +243 -0
  125. package/lib/web/public/timeline/index.html +195 -0
  126. package/lib/web/routes/api.js +499 -0
  127. package/lib/web/routes/logs.js +20 -0
  128. package/lib/web/routes/metrics.js +99 -0
  129. package/lib/web/server.js +183 -0
  130. package/lib/web/ws/handler.js +65 -0
  131. package/package.json +67 -0
  132. package/templates/agent-architect.md +69 -0
  133. package/templates/agent-gemini-pm.md +49 -0
  134. package/templates/agent-gemini-reviewer.md +52 -0
  135. package/templates/copilot-instructions.md +36 -0
  136. package/templates/pipelines/mobile.json +27 -0
  137. package/templates/pipelines/nodejs-api.json +27 -0
  138. package/templates/pipelines/python.json +27 -0
  139. package/templates/pipelines/react.json +27 -0
  140. package/templates/pipelines/salesforce.json +27 -0
  141. package/templates/role-gemini.md +97 -0
  142. package/templates/skill-architect.md +114 -0
  143. package/templates/skill-browser-qa.md +50 -0
  144. package/templates/skill-bug-from-qa.md +58 -0
  145. package/templates/skill-chatbot.md +93 -0
  146. package/templates/skill-implement.md +78 -0
  147. package/templates/skill-openclaw.md +174 -0
  148. package/templates/skill-payment.md +110 -0
  149. package/templates/skill-pm-spec.md +77 -0
  150. package/templates/skill-requirement-capture.md +97 -0
  151. package/templates/skill-review.md +108 -0
  152. package/templates/skill-reviewer-qa.md +44 -0
  153. package/templates/skill-suggestion.md +45 -0
  154. package/templates/skill-template.md +142 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * UX Analyzer — Analyzes QA history and usage patterns to identify
3
+ * improvement opportunities for the Suggestion Agent.
4
+ */
5
+
6
+ import { existsSync, readdirSync, readFileSync } from 'fs';
7
+ import { resolve } from 'path';
8
+ import { getWorkflowDir } from './pipeline.js';
9
+
10
+ export function analyzeQAHistory() {
11
+ const qaDir = resolve(getWorkflowDir(), 'qa-reports');
12
+ if (!existsSync(qaDir)) return null;
13
+
14
+ const reports = readdirSync(qaDir)
15
+ .filter(f => f.startsWith('QA-') && f.endsWith('.json'))
16
+ .sort()
17
+ .reverse()
18
+ .slice(0, 10)
19
+ .map(f => {
20
+ try { return JSON.parse(readFileSync(resolve(qaDir, f), 'utf8')); }
21
+ catch { return null; }
22
+ })
23
+ .filter(Boolean);
24
+
25
+ if (reports.length === 0) return null;
26
+
27
+ // Recurring failures
28
+ const failCount = {};
29
+ for (const report of reports) {
30
+ for (const fail of (report.failed || [])) {
31
+ try {
32
+ const key = new URL(fail.url).pathname;
33
+ failCount[key] = (failCount[key] || 0) + 1;
34
+ } catch { /* invalid URL */ }
35
+ }
36
+ }
37
+
38
+ // Recurring warnings
39
+ const warnCount = {};
40
+ for (const report of reports) {
41
+ for (const warn of (report.warnings || [])) {
42
+ for (const w of (warn.warnings || [])) {
43
+ warnCount[w] = (warnCount[w] || 0) + 1;
44
+ }
45
+ }
46
+ }
47
+
48
+ // Routes tested
49
+ const allRoutes = new Set();
50
+ for (const report of reports) {
51
+ for (const p of (report.passed || [])) {
52
+ try { allRoutes.add(new URL(p.url).pathname); } catch { /* skip */ }
53
+ }
54
+ for (const f of (report.failed || [])) {
55
+ try { allRoutes.add(new URL(f.url).pathname); } catch { /* skip */ }
56
+ }
57
+ }
58
+
59
+ // Average pass rate
60
+ const avgPassRate = Math.round(
61
+ reports.reduce((sum, r) => sum + (r.summary?.passRate || 0), 0) / reports.length
62
+ );
63
+
64
+ return {
65
+ avgPassRate,
66
+ recurringFailures: Object.entries(failCount)
67
+ .sort((a, b) => b[1] - a[1])
68
+ .slice(0, 5)
69
+ .map(([path, count]) => ({ path, count })),
70
+ recurringWarnings: Object.entries(warnCount)
71
+ .sort((a, b) => b[1] - a[1])
72
+ .slice(0, 5)
73
+ .map(([warning, count]) => ({ warning, count })),
74
+ testedRoutes: [...allRoutes],
75
+ totalReports: reports.length,
76
+ };
77
+ }
78
+
79
+ export function analyzeActivityLogs() {
80
+ try {
81
+ const logDir = resolve(getWorkflowDir(), 'logs');
82
+ if (!existsSync(logDir)) return null;
83
+
84
+ const logs = readdirSync(logDir)
85
+ .filter(f => f.endsWith('.log'))
86
+ .sort()
87
+ .reverse()
88
+ .slice(0, 5)
89
+ .map(f => readFileSync(resolve(logDir, f), 'utf8'))
90
+ .join('\n');
91
+
92
+ const errorLines = logs.split('\n').filter(l => l.includes('[ERROR]') || l.includes('[WARN]'));
93
+
94
+ return {
95
+ totalErrorLines: errorLines.length,
96
+ sampleErrors: errorLines.slice(0, 10),
97
+ };
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
@@ -0,0 +1,83 @@
1
+ import { createHmac } from 'crypto';
2
+ import { getConfig } from '../config.js';
3
+ import { bus } from '../shared/event-bus.js';
4
+
5
+ export function signPayload(payload, secret) {
6
+ const body = typeof payload === 'string' ? payload : JSON.stringify(payload);
7
+ return createHmac('sha256', secret).update(body).digest('hex');
8
+ }
9
+
10
+ export async function emitWebhook(url, event, payload, secret) {
11
+ try {
12
+ const body = JSON.stringify(payload);
13
+ const headers = {
14
+ 'Content-Type': 'application/json',
15
+ 'X-AICC-Event': event
16
+ };
17
+ if (secret) {
18
+ headers['X-AICC-Signature'] = signPayload(body, secret);
19
+ }
20
+ const res = await fetch(url, { method: 'POST', headers, body });
21
+ return { success: res.ok, statusCode: res.status, error: null };
22
+ } catch (err) {
23
+ return { success: false, statusCode: null, error: err.message };
24
+ }
25
+ }
26
+
27
+ export async function emitWithRetry(url, event, payload, secret, maxRetries = 3) {
28
+ let result;
29
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
30
+ result = await emitWebhook(url, event, payload, secret);
31
+ if (result.success) return result;
32
+ if (attempt < maxRetries) {
33
+ const delay = Math.pow(4, attempt) * 1000; // 1s, 4s, 16s
34
+ await new Promise(r => setTimeout(r, delay));
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+
40
+ export function getWebhookConfig() {
41
+ try {
42
+ const config = getConfig();
43
+ const hooks = config.webhooks || [];
44
+ return hooks.map(h => ({
45
+ ...h,
46
+ secret: h.secret ? '****' : undefined
47
+ }));
48
+ } catch {
49
+ return [];
50
+ }
51
+ }
52
+
53
+ export async function testWebhook(url, secret) {
54
+ return emitWebhook(url, 'ping', {
55
+ type: 'ping',
56
+ timestamp: new Date().toISOString(),
57
+ message: 'AICC webhook test'
58
+ }, secret);
59
+ }
60
+
61
+ export function initWebhooks() {
62
+ let hooks;
63
+ try {
64
+ const config = getConfig();
65
+ hooks = config.webhooks || [];
66
+ } catch {
67
+ return;
68
+ }
69
+ if (!hooks.length) return;
70
+
71
+ bus.on('pipeline-event', async (data) => {
72
+ for (const hook of hooks) {
73
+ const events = hook.events || [];
74
+ if (events.includes(data.type)) {
75
+ try {
76
+ await emitWithRetry(hook.url, data.type, data, hook.secret);
77
+ } catch {
78
+ // Webhook failures must never crash the app
79
+ }
80
+ }
81
+ }
82
+ });
83
+ }
@@ -0,0 +1,417 @@
1
+ /* AI Control Center Dashboard — dark terminal-inspired theme */
2
+
3
+ :root {
4
+ --bg: #0d1117;
5
+ --bg-surface: #161b22;
6
+ --bg-card: #1c2128;
7
+ --bg-hover: #21262d;
8
+ --border: #30363d;
9
+ --text: #e6edf3;
10
+ --text-dim: #7d8590;
11
+ --text-muted: #484f58;
12
+ --accent: #58a6ff;
13
+ --green: #3fb950;
14
+ --yellow: #d29922;
15
+ --red: #f85149;
16
+ --magenta: #bc8cff;
17
+ --cyan: #79c0ff;
18
+ --blue: #58a6ff;
19
+ --mono: 'SF Mono', 'Cascadia Code', 'JetBrains Mono', 'Fira Code', Menlo, monospace;
20
+ --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
21
+ --radius: 6px;
22
+ }
23
+
24
+ * { margin: 0; padding: 0; box-sizing: border-box; }
25
+
26
+ body {
27
+ font-family: var(--sans);
28
+ background: var(--bg);
29
+ color: var(--text);
30
+ min-height: 100vh;
31
+ display: flex;
32
+ flex-direction: column;
33
+ }
34
+
35
+ /* ─── Header ─────────────────────────────────────────────────────────────── */
36
+
37
+ header {
38
+ display: flex;
39
+ justify-content: space-between;
40
+ align-items: center;
41
+ padding: 12px 24px;
42
+ background: var(--bg-surface);
43
+ border-bottom: 1px solid var(--border);
44
+ }
45
+
46
+ .header-brand {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 8px;
50
+ }
51
+
52
+ .diamond { color: var(--accent); font-size: 18px; }
53
+ .brand { font-weight: 700; font-size: 16px; }
54
+ .subtitle { color: var(--text-dim); font-size: 13px; }
55
+
56
+ .header-status {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 12px;
60
+ font-family: var(--mono);
61
+ font-size: 12px;
62
+ }
63
+
64
+ .badge {
65
+ padding: 2px 10px;
66
+ border-radius: 12px;
67
+ font-size: 11px;
68
+ font-weight: 600;
69
+ text-transform: uppercase;
70
+ letter-spacing: 0.5px;
71
+ }
72
+
73
+ .badge-idle { background: var(--bg-hover); color: var(--text-dim); }
74
+ .badge-progress { background: #1a3a2a; color: var(--green); }
75
+ .badge-review { background: #2a2520; color: var(--yellow); }
76
+ .badge-approved { background: #1a3a2a; color: var(--green); }
77
+ .badge-rejected { background: #3a1a1a; color: var(--red); }
78
+ .badge-deployed { background: #1a2a3a; color: var(--cyan); }
79
+
80
+ .ws-dot {
81
+ width: 8px;
82
+ height: 8px;
83
+ border-radius: 50%;
84
+ display: inline-block;
85
+ }
86
+ .ws-connected { background: var(--green); }
87
+ .ws-disconnected { background: var(--red); }
88
+
89
+ .dim { color: var(--text-dim); }
90
+
91
+ /* ─── Nav ────────────────────────────────────────────────────────────────── */
92
+
93
+ nav {
94
+ display: flex;
95
+ gap: 0;
96
+ padding: 0 24px;
97
+ background: var(--bg-surface);
98
+ border-bottom: 1px solid var(--border);
99
+ }
100
+
101
+ .nav-link {
102
+ padding: 10px 16px;
103
+ color: var(--text-dim);
104
+ text-decoration: none;
105
+ font-size: 13px;
106
+ font-weight: 500;
107
+ border-bottom: 2px solid transparent;
108
+ transition: color 0.15s, border-color 0.15s;
109
+ }
110
+
111
+ .nav-link:hover { color: var(--text); }
112
+ .nav-link.active {
113
+ color: var(--accent);
114
+ border-bottom-color: var(--accent);
115
+ }
116
+
117
+ /* ─── Main ───────────────────────────────────────────────────────────────── */
118
+
119
+ main {
120
+ flex: 1;
121
+ padding: 24px;
122
+ max-width: 960px;
123
+ margin: 0 auto;
124
+ width: 100%;
125
+ }
126
+
127
+ /* ─── Cards ──────────────────────────────────────────────────────────────── */
128
+
129
+ .card {
130
+ background: var(--bg-card);
131
+ border: 1px solid var(--border);
132
+ border-radius: var(--radius);
133
+ padding: 20px;
134
+ margin-bottom: 16px;
135
+ }
136
+
137
+ .card-title {
138
+ font-size: 14px;
139
+ font-weight: 600;
140
+ margin-bottom: 12px;
141
+ color: var(--text);
142
+ }
143
+
144
+ /* ─── Pipeline Progress ──────────────────────────────────────────────────── */
145
+
146
+ .pipeline-stages {
147
+ display: flex;
148
+ gap: 4px;
149
+ margin: 16px 0;
150
+ }
151
+
152
+ .stage-step {
153
+ flex: 1;
154
+ text-align: center;
155
+ padding: 8px 4px;
156
+ font-size: 11px;
157
+ font-family: var(--mono);
158
+ border-radius: 4px;
159
+ background: var(--bg-hover);
160
+ color: var(--text-muted);
161
+ transition: background 0.2s, color 0.2s;
162
+ }
163
+
164
+ .stage-step.active {
165
+ background: var(--accent);
166
+ color: #fff;
167
+ }
168
+
169
+ .stage-step.completed {
170
+ background: #1a3a2a;
171
+ color: var(--green);
172
+ }
173
+
174
+ /* ─── Forms ──────────────────────────────────────────────────────────────── */
175
+
176
+ .form-group {
177
+ margin-bottom: 16px;
178
+ }
179
+
180
+ label {
181
+ display: block;
182
+ font-size: 12px;
183
+ color: var(--text-dim);
184
+ margin-bottom: 6px;
185
+ font-weight: 500;
186
+ }
187
+
188
+ input[type="text"], textarea, select {
189
+ width: 100%;
190
+ padding: 10px 12px;
191
+ background: var(--bg);
192
+ border: 1px solid var(--border);
193
+ border-radius: var(--radius);
194
+ color: var(--text);
195
+ font-family: var(--mono);
196
+ font-size: 13px;
197
+ }
198
+
199
+ input:focus, textarea:focus, select:focus {
200
+ outline: none;
201
+ border-color: var(--accent);
202
+ }
203
+
204
+ textarea { resize: vertical; min-height: 80px; }
205
+
206
+ /* ─── Buttons ────────────────────────────────────────────────────────────── */
207
+
208
+ .btn {
209
+ padding: 8px 16px;
210
+ border: 1px solid var(--border);
211
+ border-radius: var(--radius);
212
+ background: var(--bg-hover);
213
+ color: var(--text);
214
+ font-size: 13px;
215
+ font-weight: 500;
216
+ cursor: pointer;
217
+ transition: background 0.15s;
218
+ }
219
+
220
+ .btn:hover { background: var(--border); }
221
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
222
+
223
+ .btn-primary {
224
+ background: var(--accent);
225
+ border-color: var(--accent);
226
+ color: #fff;
227
+ }
228
+ .btn-primary:hover { background: #4090e0; }
229
+
230
+ .btn-success {
231
+ background: #238636;
232
+ border-color: #238636;
233
+ color: #fff;
234
+ }
235
+ .btn-success:hover { background: #2ea043; }
236
+
237
+ .btn-danger {
238
+ background: #da3633;
239
+ border-color: #da3633;
240
+ color: #fff;
241
+ }
242
+ .btn-danger:hover { background: #f85149; }
243
+
244
+ .btn-group { display: flex; gap: 8px; margin-top: 12px; }
245
+
246
+ /* ─── Log Viewer ─────────────────────────────────────────────────────────── */
247
+
248
+ .log-viewer {
249
+ background: var(--bg);
250
+ border: 1px solid var(--border);
251
+ border-radius: var(--radius);
252
+ padding: 12px;
253
+ max-height: 500px;
254
+ overflow-y: auto;
255
+ font-family: var(--mono);
256
+ font-size: 12px;
257
+ line-height: 1.6;
258
+ white-space: pre-wrap;
259
+ word-break: break-all;
260
+ }
261
+
262
+ .log-line { padding: 1px 0; }
263
+ .log-gemini { color: var(--cyan); }
264
+ .log-claude { color: var(--magenta); }
265
+ .log-copilot { color: var(--blue); }
266
+ .log-pipeline { color: var(--yellow); }
267
+ .log-error { color: var(--red); }
268
+ .log-success { color: var(--green); }
269
+
270
+ /* ─── Filter Tabs ────────────────────────────────────────────────────────── */
271
+
272
+ .filter-tabs {
273
+ display: flex;
274
+ gap: 8px;
275
+ margin-bottom: 12px;
276
+ }
277
+
278
+ .filter-tab {
279
+ padding: 4px 12px;
280
+ border-radius: 12px;
281
+ font-size: 11px;
282
+ font-weight: 600;
283
+ cursor: pointer;
284
+ background: var(--bg-hover);
285
+ color: var(--text-dim);
286
+ border: none;
287
+ }
288
+
289
+ .filter-tab.active { background: var(--accent); color: #fff; }
290
+ .filter-tab:hover { background: var(--border); color: var(--text); }
291
+
292
+ /* ─── Health Check ───────────────────────────────────────────────────────── */
293
+
294
+ .health-row {
295
+ display: flex;
296
+ align-items: center;
297
+ gap: 12px;
298
+ padding: 6px 0;
299
+ font-family: var(--mono);
300
+ font-size: 13px;
301
+ }
302
+
303
+ .health-icon { width: 20px; text-align: center; }
304
+ .health-ok { color: var(--green); }
305
+ .health-fail { color: var(--red); }
306
+ .health-warn { color: var(--yellow); }
307
+ .health-label { width: 140px; font-weight: 600; }
308
+ .health-detail { color: var(--text-dim); }
309
+
310
+ /* ─── Verdict Panel ──────────────────────────────────────────────────────── */
311
+
312
+ .verdict-panel {
313
+ border-left: 3px solid var(--border);
314
+ padding: 16px 20px;
315
+ margin: 12px 0;
316
+ }
317
+
318
+ .verdict-approved { border-left-color: var(--green); }
319
+ .verdict-rejected { border-left-color: var(--red); }
320
+
321
+ .verdict-title {
322
+ font-size: 16px;
323
+ font-weight: 700;
324
+ margin-bottom: 12px;
325
+ }
326
+
327
+ .verdict-section {
328
+ margin: 8px 0;
329
+ }
330
+
331
+ .verdict-section h4 {
332
+ font-size: 12px;
333
+ color: var(--text-dim);
334
+ margin-bottom: 4px;
335
+ text-transform: uppercase;
336
+ letter-spacing: 0.5px;
337
+ }
338
+
339
+ .verdict-list {
340
+ list-style: none;
341
+ padding: 0;
342
+ }
343
+
344
+ .verdict-list li {
345
+ padding: 3px 0;
346
+ font-size: 13px;
347
+ font-family: var(--mono);
348
+ }
349
+
350
+ .verdict-list li::before { content: ' '; }
351
+
352
+ /* ─── Toast Notifications ─────────────────────────────────────────────────── */
353
+
354
+ .toast-container {
355
+ position: fixed;
356
+ top: 16px;
357
+ right: 16px;
358
+ z-index: 1000;
359
+ display: flex;
360
+ flex-direction: column;
361
+ gap: 8px;
362
+ }
363
+
364
+ .toast {
365
+ padding: 12px 16px;
366
+ border-radius: var(--radius);
367
+ background: var(--bg-card);
368
+ border: 1px solid var(--border);
369
+ font-size: 13px;
370
+ animation: slideIn 0.2s ease-out;
371
+ max-width: 320px;
372
+ }
373
+
374
+ .toast-success { border-left: 3px solid var(--green); }
375
+ .toast-error { border-left: 3px solid var(--red); }
376
+ .toast-info { border-left: 3px solid var(--accent); }
377
+
378
+ @keyframes slideIn {
379
+ from { transform: translateX(100%); opacity: 0; }
380
+ to { transform: translateX(0); opacity: 1; }
381
+ }
382
+
383
+ /* ─── Spinner ────────────────────────────────────────────────────────────── */
384
+
385
+ .spinner {
386
+ display: inline-block;
387
+ width: 14px;
388
+ height: 14px;
389
+ border: 2px solid var(--border);
390
+ border-top-color: var(--accent);
391
+ border-radius: 50%;
392
+ animation: spin 0.6s linear infinite;
393
+ }
394
+
395
+ @keyframes spin {
396
+ to { transform: rotate(360deg); }
397
+ }
398
+
399
+ /* ─── Footer ─────────────────────────────────────────────────────────────── */
400
+
401
+ footer {
402
+ display: flex;
403
+ justify-content: space-between;
404
+ padding: 8px 24px;
405
+ background: var(--bg-surface);
406
+ border-top: 1px solid var(--border);
407
+ font-size: 11px;
408
+ }
409
+
410
+ /* ─── Responsive ─────────────────────────────────────────────────────────── */
411
+
412
+ @media (max-width: 640px) {
413
+ header { flex-direction: column; gap: 8px; }
414
+ nav { overflow-x: auto; }
415
+ .pipeline-stages { flex-wrap: wrap; }
416
+ main { padding: 16px; }
417
+ }
@@ -0,0 +1,44 @@
1
+ // Dark mode toggle for AICC dashboard
2
+ (function() {
3
+ const STORAGE_KEY = 'aicc-dark-mode';
4
+
5
+ function isDarkMode() {
6
+ const stored = localStorage.getItem(STORAGE_KEY);
7
+ if (stored !== null) return stored === 'true';
8
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
9
+ }
10
+
11
+ function applyTheme(dark) {
12
+ document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
13
+ document.body.classList.toggle('dark-mode', dark);
14
+ document.body.classList.toggle('light-mode', !dark);
15
+ localStorage.setItem(STORAGE_KEY, dark);
16
+
17
+ // Update toggle button if exists
18
+ const btn = document.getElementById('dark-mode-toggle');
19
+ if (btn) btn.textContent = dark ? '☀️ Light' : '🌙 Dark';
20
+ }
21
+
22
+ function toggle() {
23
+ applyTheme(!isDarkMode());
24
+ }
25
+
26
+ function createToggleButton() {
27
+ const btn = document.createElement('button');
28
+ btn.id = 'dark-mode-toggle';
29
+ btn.onclick = toggle;
30
+ btn.style.cssText = 'position:fixed;bottom:1rem;right:1rem;padding:0.5rem 1rem;background:#334155;border:1px solid #475569;border-radius:8px;color:#e2e8f0;cursor:pointer;font-size:0.8rem;z-index:1000;';
31
+ document.body.appendChild(btn);
32
+ }
33
+
34
+ // Auto-apply on load
35
+ if (document.readyState === 'loading') {
36
+ document.addEventListener('DOMContentLoaded', () => { applyTheme(isDarkMode()); createToggleButton(); });
37
+ } else {
38
+ applyTheme(isDarkMode());
39
+ createToggleButton();
40
+ }
41
+
42
+ // Export for module usage
43
+ window.darkMode = { toggle, isDarkMode, applyTheme };
44
+ })();