skimpyclaw 0.1.0
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/README.md +230 -0
- package/dist/__tests__/agent.test.d.ts +1 -0
- package/dist/__tests__/agent.test.js +131 -0
- package/dist/__tests__/api.test.d.ts +1 -0
- package/dist/__tests__/api.test.js +1227 -0
- package/dist/__tests__/audit.test.d.ts +1 -0
- package/dist/__tests__/audit.test.js +122 -0
- package/dist/__tests__/cache.test.d.ts +1 -0
- package/dist/__tests__/cache.test.js +65 -0
- package/dist/__tests__/channels.test.d.ts +1 -0
- package/dist/__tests__/channels.test.js +85 -0
- package/dist/__tests__/cli.integration.test.d.ts +1 -0
- package/dist/__tests__/cli.integration.test.js +16 -0
- package/dist/__tests__/cli.test.d.ts +1 -0
- package/dist/__tests__/cli.test.js +230 -0
- package/dist/__tests__/code-agents-executor.test.d.ts +1 -0
- package/dist/__tests__/code-agents-executor.test.js +75 -0
- package/dist/__tests__/code-agents-orchestrator.test.d.ts +1 -0
- package/dist/__tests__/code-agents-orchestrator.test.js +149 -0
- package/dist/__tests__/code-agents-parser.test.d.ts +1 -0
- package/dist/__tests__/code-agents-parser.test.js +39 -0
- package/dist/__tests__/code-agents-utils.test.d.ts +1 -0
- package/dist/__tests__/code-agents-utils.test.js +41 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +46 -0
- package/dist/__tests__/cron.test.d.ts +1 -0
- package/dist/__tests__/cron.test.js +66 -0
- package/dist/__tests__/dashboard-mode.test.d.ts +1 -0
- package/dist/__tests__/dashboard-mode.test.js +145 -0
- package/dist/__tests__/dashboard.test.d.ts +1 -0
- package/dist/__tests__/dashboard.test.js +43 -0
- package/dist/__tests__/doctor.formatters.test.d.ts +1 -0
- package/dist/__tests__/doctor.formatters.test.js +65 -0
- package/dist/__tests__/doctor.index.test.d.ts +1 -0
- package/dist/__tests__/doctor.index.test.js +48 -0
- package/dist/__tests__/doctor.runner.test.d.ts +1 -0
- package/dist/__tests__/doctor.runner.test.js +204 -0
- package/dist/__tests__/exec-approval.test.d.ts +1 -0
- package/dist/__tests__/exec-approval.test.js +323 -0
- package/dist/__tests__/file-lock.test.d.ts +1 -0
- package/dist/__tests__/file-lock.test.js +92 -0
- package/dist/__tests__/langfuse.test.d.ts +1 -0
- package/dist/__tests__/langfuse.test.js +40 -0
- package/dist/__tests__/model-selection.test.d.ts +1 -0
- package/dist/__tests__/model-selection.test.js +62 -0
- package/dist/__tests__/orchestrator.test.d.ts +1 -0
- package/dist/__tests__/orchestrator.test.js +425 -0
- package/dist/__tests__/providers-init.test.d.ts +1 -0
- package/dist/__tests__/providers-init.test.js +32 -0
- package/dist/__tests__/providers-routing.test.d.ts +1 -0
- package/dist/__tests__/providers-routing.test.js +25 -0
- package/dist/__tests__/providers-utils.test.d.ts +1 -0
- package/dist/__tests__/providers-utils.test.js +54 -0
- package/dist/__tests__/security.test.d.ts +1 -0
- package/dist/__tests__/security.test.js +22 -0
- package/dist/__tests__/sessions.test.d.ts +1 -0
- package/dist/__tests__/sessions.test.js +147 -0
- package/dist/__tests__/setup.test.d.ts +1 -0
- package/dist/__tests__/setup.test.js +114 -0
- package/dist/__tests__/skills.test.d.ts +1 -0
- package/dist/__tests__/skills.test.js +333 -0
- package/dist/__tests__/subagent.test.d.ts +1 -0
- package/dist/__tests__/subagent.test.js +240 -0
- package/dist/__tests__/telegram-utils.test.d.ts +1 -0
- package/dist/__tests__/telegram-utils.test.js +22 -0
- package/dist/__tests__/telegram.test.d.ts +1 -0
- package/dist/__tests__/telegram.test.js +42 -0
- package/dist/__tests__/token-efficiency.test.d.ts +1 -0
- package/dist/__tests__/token-efficiency.test.js +38 -0
- package/dist/__tests__/tool-guard.test.d.ts +1 -0
- package/dist/__tests__/tool-guard.test.js +105 -0
- package/dist/__tests__/tools.test.d.ts +1 -0
- package/dist/__tests__/tools.test.js +589 -0
- package/dist/__tests__/usage.test.d.ts +1 -0
- package/dist/__tests__/usage.test.js +197 -0
- package/dist/__tests__/voice.test.d.ts +1 -0
- package/dist/__tests__/voice.test.js +214 -0
- package/dist/agent.d.ts +24 -0
- package/dist/agent.js +269 -0
- package/dist/api.d.ts +3 -0
- package/dist/api.js +943 -0
- package/dist/audit.d.ts +26 -0
- package/dist/audit.js +121 -0
- package/dist/cache.d.ts +8 -0
- package/dist/cache.js +24 -0
- package/dist/channels/telegram/handlers.d.ts +41 -0
- package/dist/channels/telegram/handlers.js +498 -0
- package/dist/channels/telegram/index.d.ts +14 -0
- package/dist/channels/telegram/index.js +326 -0
- package/dist/channels/telegram/types.d.ts +26 -0
- package/dist/channels/telegram/types.js +31 -0
- package/dist/channels/telegram/utils.d.ts +25 -0
- package/dist/channels/telegram/utils.js +256 -0
- package/dist/channels.d.ts +11 -0
- package/dist/channels.js +118 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +768 -0
- package/dist/code-agents/executor.d.ts +5 -0
- package/dist/code-agents/executor.js +463 -0
- package/dist/code-agents/index.d.ts +22 -0
- package/dist/code-agents/index.js +199 -0
- package/dist/code-agents/orchestrator.d.ts +23 -0
- package/dist/code-agents/orchestrator.js +403 -0
- package/dist/code-agents/parser.d.ts +21 -0
- package/dist/code-agents/parser.js +197 -0
- package/dist/code-agents/registry.d.ts +27 -0
- package/dist/code-agents/registry.js +147 -0
- package/dist/code-agents/types.d.ts +66 -0
- package/dist/code-agents/types.js +4 -0
- package/dist/code-agents/utils.d.ts +36 -0
- package/dist/code-agents/utils.js +236 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +123 -0
- package/dist/cron.d.ts +49 -0
- package/dist/cron.js +400 -0
- package/dist/dashboard/assets/index-CZJCvMSN.js +65 -0
- package/dist/dashboard/assets/index-EAg6lqF5.css +1 -0
- package/dist/dashboard/favicon.svg +3 -0
- package/dist/dashboard/index.html +21 -0
- package/dist/dashboard-frontend.d.ts +7 -0
- package/dist/dashboard-frontend.js +86 -0
- package/dist/dashboard.d.ts +8 -0
- package/dist/dashboard.js +4071 -0
- package/dist/digests.d.ts +36 -0
- package/dist/digests.js +338 -0
- package/dist/discord.d.ts +8 -0
- package/dist/discord.js +828 -0
- package/dist/doctor/checks.d.ts +18 -0
- package/dist/doctor/checks.js +368 -0
- package/dist/doctor/formatters.d.ts +3 -0
- package/dist/doctor/formatters.js +44 -0
- package/dist/doctor/index.d.ts +8 -0
- package/dist/doctor/index.js +7 -0
- package/dist/doctor/runner.d.ts +3 -0
- package/dist/doctor/runner.js +109 -0
- package/dist/doctor/types.d.ts +20 -0
- package/dist/doctor/types.js +1 -0
- package/dist/exec-approval.d.ts +101 -0
- package/dist/exec-approval.js +432 -0
- package/dist/file-lock.d.ts +34 -0
- package/dist/file-lock.js +81 -0
- package/dist/gateway.d.ts +8 -0
- package/dist/gateway.js +114 -0
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.js +101 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +75 -0
- package/dist/langfuse.d.ts +34 -0
- package/dist/langfuse.js +145 -0
- package/dist/mcp-context-a8c.d.ts +13 -0
- package/dist/mcp-context-a8c.js +34 -0
- package/dist/model-selection.d.ts +18 -0
- package/dist/model-selection.js +50 -0
- package/dist/orchestrator.d.ts +15 -0
- package/dist/orchestrator.js +676 -0
- package/dist/providers/anthropic.d.ts +7 -0
- package/dist/providers/anthropic.js +319 -0
- package/dist/providers/codex.d.ts +17 -0
- package/dist/providers/codex.js +508 -0
- package/dist/providers/content.d.ts +21 -0
- package/dist/providers/content.js +55 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.js +138 -0
- package/dist/providers/observability.d.ts +19 -0
- package/dist/providers/observability.js +94 -0
- package/dist/providers/openai.d.ts +10 -0
- package/dist/providers/openai.js +310 -0
- package/dist/providers/tool-guard.d.ts +30 -0
- package/dist/providers/tool-guard.js +89 -0
- package/dist/providers/types.d.ts +34 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/utils.d.ts +65 -0
- package/dist/providers/utils.js +199 -0
- package/dist/security.d.ts +8 -0
- package/dist/security.js +113 -0
- package/dist/service.d.ts +8 -0
- package/dist/service.js +38 -0
- package/dist/sessions.d.ts +35 -0
- package/dist/sessions.js +142 -0
- package/dist/setup.d.ts +36 -0
- package/dist/setup.js +821 -0
- package/dist/skills-types.d.ts +65 -0
- package/dist/skills-types.js +2 -0
- package/dist/skills.d.ts +32 -0
- package/dist/skills.js +260 -0
- package/dist/subagent.d.ts +19 -0
- package/dist/subagent.js +376 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +11 -0
- package/dist/tools/bash-tool.d.ts +3 -0
- package/dist/tools/bash-tool.js +59 -0
- package/dist/tools/browser-tool.d.ts +3 -0
- package/dist/tools/browser-tool.js +265 -0
- package/dist/tools/definitions.d.ts +432 -0
- package/dist/tools/definitions.js +181 -0
- package/dist/tools/execute-context.d.ts +26 -0
- package/dist/tools/execute-context.js +1 -0
- package/dist/tools/file-tools.d.ts +8 -0
- package/dist/tools/file-tools.js +67 -0
- package/dist/tools/path-utils.d.ts +1 -0
- package/dist/tools/path-utils.js +8 -0
- package/dist/tools.d.ts +24 -0
- package/dist/tools.js +281 -0
- package/dist/types.d.ts +259 -0
- package/dist/types.js +2 -0
- package/dist/usage.d.ts +76 -0
- package/dist/usage.js +150 -0
- package/dist/voice.d.ts +37 -0
- package/dist/voice.js +461 -0
- package/package.json +70 -0
- package/templates/AGENTS.md +38 -0
- package/templates/BOOT.md +23 -0
- package/templates/BOOTSTRAP.md +26 -0
- package/templates/HEARTBEAT.md +5 -0
- package/templates/IDENTITY.md +5 -0
- package/templates/MEMORY.md +24 -0
- package/templates/SOUL.md +92 -0
- package/templates/TOOLS.md +30 -0
- package/templates/USER.md +31 -0
|
@@ -0,0 +1,4071 @@
|
|
|
1
|
+
// Dashboard frontend - serves the single-page dashboard UI
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { extname, isAbsolute, join, relative, resolve } from 'path';
|
|
4
|
+
export function registerDashboard(fastify, options = {}) {
|
|
5
|
+
const mode = options.mode ?? 'legacy';
|
|
6
|
+
const botName = options.botName ?? 'SkimpyClaw';
|
|
7
|
+
const botEmoji = options.botEmoji ?? '👙🦞';
|
|
8
|
+
const frameworkDistDir = resolve(options.frameworkDistDir ?? join(process.cwd(), 'dist', 'dashboard'));
|
|
9
|
+
const frameworkIndexPath = join(frameworkDistDir, 'index.html');
|
|
10
|
+
if (mode === 'framework' && existsSync(frameworkIndexPath)) {
|
|
11
|
+
const serveFrameworkIndex = async (_request, reply) => {
|
|
12
|
+
try {
|
|
13
|
+
const html = readFileSync(frameworkIndexPath, 'utf-8')
|
|
14
|
+
.replace('__SKIMPY_BOT_NAME__', escapeForInlineScript(botName))
|
|
15
|
+
.replace('__SKIMPY_BOT_EMOJI__', escapeForInlineScript(botEmoji));
|
|
16
|
+
reply.type('text/html').send(html);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
reply.code(500).send('Framework dashboard failed to load');
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
fastify.get('/dashboard', serveFrameworkIndex);
|
|
23
|
+
fastify.get('/dashboard/*', serveFrameworkIndex);
|
|
24
|
+
fastify.get('/assets/*', async (request, reply) => {
|
|
25
|
+
const relPath = request.params['*'];
|
|
26
|
+
const assetsBaseDir = resolve(frameworkDistDir, 'assets');
|
|
27
|
+
const filePath = resolve(assetsBaseDir, relPath);
|
|
28
|
+
const rel = relative(assetsBaseDir, filePath);
|
|
29
|
+
if (!rel || rel.startsWith('..') || isAbsolute(rel) || !existsSync(filePath)) {
|
|
30
|
+
reply.code(404).send('Not found');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const buffer = readFileSync(filePath);
|
|
34
|
+
reply.type(getMimeType(filePath)).send(buffer);
|
|
35
|
+
});
|
|
36
|
+
// Serve root-level static files from dist (e.g. favicon.svg)
|
|
37
|
+
// Only allows known safe extensions — no directory traversal
|
|
38
|
+
const ALLOWED_ROOT_STATIC_EXTS = new Set(['.svg', '.png', '.ico', '.webmanifest', '.xml']);
|
|
39
|
+
fastify.get('/:file', async (request, reply) => {
|
|
40
|
+
const fileName = request.params.file;
|
|
41
|
+
const ext = extname(fileName).toLowerCase();
|
|
42
|
+
if (!ALLOWED_ROOT_STATIC_EXTS.has(ext)) {
|
|
43
|
+
// Not a static file request — don't handle it
|
|
44
|
+
reply.callNotFound();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const filePath = resolve(frameworkDistDir, fileName);
|
|
48
|
+
const rel = relative(frameworkDistDir, filePath);
|
|
49
|
+
if (!rel || rel.startsWith('..') || isAbsolute(rel) || rel.includes('/') || !existsSync(filePath)) {
|
|
50
|
+
reply.code(404).send('Not found');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const buffer = readFileSync(filePath);
|
|
54
|
+
reply.type(getMimeType(filePath)).send(buffer);
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (mode === 'framework') {
|
|
59
|
+
console.warn(`[dashboard] Framework mode requested, but ${frameworkIndexPath} is missing. Falling back to legacy.`);
|
|
60
|
+
}
|
|
61
|
+
fastify.get('/dashboard', async (_request, reply) => {
|
|
62
|
+
reply.type('text/html').send(DASHBOARD_HTML);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function escapeForInlineScript(value) {
|
|
66
|
+
return value
|
|
67
|
+
.replace(/\\/g, '\\\\')
|
|
68
|
+
.replace(/"/g, '\\"')
|
|
69
|
+
.replace(/</g, '\\u003c')
|
|
70
|
+
.replace(/>/g, '\\u003e');
|
|
71
|
+
}
|
|
72
|
+
function getMimeType(filePath) {
|
|
73
|
+
const ext = extname(filePath).toLowerCase();
|
|
74
|
+
if (ext === '.js')
|
|
75
|
+
return 'text/javascript; charset=utf-8';
|
|
76
|
+
if (ext === '.css')
|
|
77
|
+
return 'text/css; charset=utf-8';
|
|
78
|
+
if (ext === '.json')
|
|
79
|
+
return 'application/json; charset=utf-8';
|
|
80
|
+
if (ext === '.map')
|
|
81
|
+
return 'application/json; charset=utf-8';
|
|
82
|
+
if (ext === '.svg')
|
|
83
|
+
return 'image/svg+xml';
|
|
84
|
+
if (ext === '.png')
|
|
85
|
+
return 'image/png';
|
|
86
|
+
if (ext === '.jpg' || ext === '.jpeg')
|
|
87
|
+
return 'image/jpeg';
|
|
88
|
+
if (ext === '.woff2')
|
|
89
|
+
return 'font/woff2';
|
|
90
|
+
if (ext === '.woff')
|
|
91
|
+
return 'font/woff';
|
|
92
|
+
if (ext === '.ttf')
|
|
93
|
+
return 'font/ttf';
|
|
94
|
+
return 'application/octet-stream';
|
|
95
|
+
}
|
|
96
|
+
const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
97
|
+
<html lang="en" data-theme="light">
|
|
98
|
+
<head>
|
|
99
|
+
<meta charset="UTF-8">
|
|
100
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
101
|
+
<title>SkimpyClaw Dashboard</title>
|
|
102
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@600;700;800&display=swap" rel="stylesheet">
|
|
103
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.min.js"></script>
|
|
104
|
+
<style>
|
|
105
|
+
/* ══════════════════════════════════════════════════
|
|
106
|
+
LIGHT THEME (default) — warm cream + sage
|
|
107
|
+
══════════════════════════════════════════════════ */
|
|
108
|
+
:root,
|
|
109
|
+
[data-theme="light"] {
|
|
110
|
+
--bg: #f5f2ee;
|
|
111
|
+
--bg-sidebar: #f5f2ee;
|
|
112
|
+
--surface: #ffffff;
|
|
113
|
+
--surface-alt: #f9f7f4;
|
|
114
|
+
--surface-hover: #f0ede8;
|
|
115
|
+
--text: #2c2825;
|
|
116
|
+
--text-dim: #5c5652;
|
|
117
|
+
--text-muted: #9b948e;
|
|
118
|
+
--accent: #4a7c6f;
|
|
119
|
+
--accent-hover: #3b6b5e;
|
|
120
|
+
--accent-soft: rgba(74, 124, 111, 0.08);
|
|
121
|
+
--accent-soft-2: rgba(74, 124, 111, 0.04);
|
|
122
|
+
--success: #4a7c6f;
|
|
123
|
+
--success-soft: rgba(74, 124, 111, 0.08);
|
|
124
|
+
--warning: #c4873a;
|
|
125
|
+
--warning-soft: rgba(196, 135, 58, 0.08);
|
|
126
|
+
--error: #c25450;
|
|
127
|
+
--error-soft: rgba(194, 84, 80, 0.06);
|
|
128
|
+
--border: rgba(0,0,0,0.06);
|
|
129
|
+
--border-light: rgba(0,0,0,0.04);
|
|
130
|
+
--shadow-sm: 0 1px 2px rgba(0,0,0,0.03);
|
|
131
|
+
--shadow: 0 2px 8px rgba(0,0,0,0.04);
|
|
132
|
+
--shadow-md: 0 4px 16px rgba(0,0,0,0.06);
|
|
133
|
+
--sidebar-active: #ffffff;
|
|
134
|
+
--sidebar-hover: rgba(0,0,0,0.03);
|
|
135
|
+
--welcome-from: #4a7c6f;
|
|
136
|
+
--welcome-to: #6ba396;
|
|
137
|
+
--welcome-text: #ffffff;
|
|
138
|
+
--log-info: #5c5652;
|
|
139
|
+
--log-warn: #b07c1a;
|
|
140
|
+
--log-error: #c25450;
|
|
141
|
+
--highlight: var(--accent);
|
|
142
|
+
--mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
|
|
143
|
+
--sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
144
|
+
--serif: 'Playfair Display', Georgia, 'Times New Roman', serif;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ══════════════════════════════════════════════════
|
|
148
|
+
DARK THEME — warm charcoal + sage
|
|
149
|
+
══════════════════════════════════════════════════ */
|
|
150
|
+
[data-theme="dark"] {
|
|
151
|
+
--bg: #1a1816;
|
|
152
|
+
--bg-sidebar: #1a1816;
|
|
153
|
+
--surface: #242120;
|
|
154
|
+
--surface-alt: #2c2928;
|
|
155
|
+
--surface-hover: #353230;
|
|
156
|
+
--text: #ede9e4;
|
|
157
|
+
--text-dim: #a8a29e;
|
|
158
|
+
--text-muted: #6d6762;
|
|
159
|
+
--accent: #7dbcab;
|
|
160
|
+
--accent-hover: #92ccbc;
|
|
161
|
+
--accent-soft: rgba(125, 188, 171, 0.12);
|
|
162
|
+
--accent-soft-2: rgba(125, 188, 171, 0.06);
|
|
163
|
+
--success: #7dbcab;
|
|
164
|
+
--success-soft: rgba(125, 188, 171, 0.1);
|
|
165
|
+
--warning: #e0a95e;
|
|
166
|
+
--warning-soft: rgba(224, 169, 94, 0.1);
|
|
167
|
+
--error: #d97a77;
|
|
168
|
+
--error-soft: rgba(217, 122, 119, 0.1);
|
|
169
|
+
--border: rgba(255,255,255,0.07);
|
|
170
|
+
--border-light: rgba(255,255,255,0.04);
|
|
171
|
+
--shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
|
|
172
|
+
--shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
173
|
+
--shadow-md: 0 4px 16px rgba(0,0,0,0.3);
|
|
174
|
+
--sidebar-active: rgba(125, 188, 171, 0.08);
|
|
175
|
+
--sidebar-hover: rgba(255,255,255,0.04);
|
|
176
|
+
--welcome-from: #3d6b5e;
|
|
177
|
+
--welcome-to: #6ba396;
|
|
178
|
+
--welcome-text: #ffffff;
|
|
179
|
+
--log-info: #a8a29e;
|
|
180
|
+
--log-warn: #e0a95e;
|
|
181
|
+
--log-error: #d97a77;
|
|
182
|
+
--highlight: var(--accent);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* ══════════════════════════════════════════════════
|
|
186
|
+
SHARED TOKENS
|
|
187
|
+
══════════════════════════════════════════════════ */
|
|
188
|
+
:root {
|
|
189
|
+
--radius: 16px;
|
|
190
|
+
--radius-md: 12px;
|
|
191
|
+
--radius-sm: 8px;
|
|
192
|
+
--sidebar-width: 260px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
196
|
+
|
|
197
|
+
body {
|
|
198
|
+
font-family: var(--sans);
|
|
199
|
+
background: var(--bg);
|
|
200
|
+
color: var(--text);
|
|
201
|
+
min-height: 100vh;
|
|
202
|
+
font-size: 14px;
|
|
203
|
+
line-height: 1.55;
|
|
204
|
+
transition: background 0.3s ease, color 0.3s ease;
|
|
205
|
+
-webkit-font-smoothing: antialiased;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
a { color: var(--accent); text-decoration: none; }
|
|
209
|
+
a:hover { text-decoration: underline; }
|
|
210
|
+
|
|
211
|
+
/* ══════════════════════════════════════════════════
|
|
212
|
+
LAYOUT SHELL
|
|
213
|
+
══════════════════════════════════════════════════ */
|
|
214
|
+
.shell {
|
|
215
|
+
display: grid;
|
|
216
|
+
grid-template-columns: var(--sidebar-width) 1fr;
|
|
217
|
+
min-height: 100vh;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* ══════════════════════════════════════════════════
|
|
221
|
+
SIDEBAR
|
|
222
|
+
══════════════════════════════════════════════════ */
|
|
223
|
+
.sidebar {
|
|
224
|
+
background: var(--bg-sidebar);
|
|
225
|
+
display: flex;
|
|
226
|
+
flex-direction: column;
|
|
227
|
+
position: sticky;
|
|
228
|
+
top: 0;
|
|
229
|
+
height: 100vh;
|
|
230
|
+
z-index: 10;
|
|
231
|
+
transition: background 0.3s ease;
|
|
232
|
+
overflow-y: auto;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.sidebar-header {
|
|
236
|
+
padding: 28px 24px 20px;
|
|
237
|
+
display: flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
gap: 12px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.sidebar-logo {
|
|
243
|
+
width: 40px; height: 40px;
|
|
244
|
+
background: linear-gradient(135deg, var(--welcome-from), var(--welcome-to));
|
|
245
|
+
border-radius: var(--radius-md);
|
|
246
|
+
display: flex; align-items: center; justify-content: center;
|
|
247
|
+
font-size: 20px;
|
|
248
|
+
box-shadow: 0 2px 8px rgba(74, 124, 111, 0.2);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.sidebar-brand {
|
|
252
|
+
display: flex; flex-direction: column;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.sidebar-brand-name {
|
|
256
|
+
font-family: var(--serif);
|
|
257
|
+
font-size: 17px; font-weight: 700; color: var(--text);
|
|
258
|
+
letter-spacing: -0.01em;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.sidebar-brand-status {
|
|
262
|
+
font-size: 11px; color: var(--success);
|
|
263
|
+
display: flex; align-items: center; gap: 5px;
|
|
264
|
+
font-weight: 500;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.sidebar-brand-status::before {
|
|
268
|
+
content: '';
|
|
269
|
+
width: 6px; height: 6px;
|
|
270
|
+
border-radius: 50%;
|
|
271
|
+
background: var(--success);
|
|
272
|
+
display: inline-block;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.sidebar-section {
|
|
276
|
+
padding: 0 12px;
|
|
277
|
+
margin-bottom: 8px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.sidebar-section-label {
|
|
281
|
+
font-size: 11px;
|
|
282
|
+
font-weight: 600;
|
|
283
|
+
color: var(--text-muted);
|
|
284
|
+
text-transform: uppercase;
|
|
285
|
+
letter-spacing: 0.06em;
|
|
286
|
+
padding: 16px 12px 8px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.sidebar-item {
|
|
290
|
+
display: flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
gap: 12px;
|
|
293
|
+
padding: 10px 12px;
|
|
294
|
+
border-radius: var(--radius-sm);
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
color: var(--text-dim);
|
|
297
|
+
font-size: 14px;
|
|
298
|
+
font-weight: 500;
|
|
299
|
+
transition: all 0.15s ease;
|
|
300
|
+
border: none;
|
|
301
|
+
background: transparent;
|
|
302
|
+
width: 100%;
|
|
303
|
+
text-align: left;
|
|
304
|
+
font-family: var(--sans);
|
|
305
|
+
position: relative;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.sidebar-item:hover {
|
|
309
|
+
background: var(--sidebar-hover);
|
|
310
|
+
color: var(--text);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.sidebar-item.active {
|
|
314
|
+
background: var(--sidebar-active);
|
|
315
|
+
color: var(--accent);
|
|
316
|
+
font-weight: 600;
|
|
317
|
+
box-shadow: var(--shadow-sm);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.sidebar-item.active::before {
|
|
321
|
+
content: '';
|
|
322
|
+
position: absolute;
|
|
323
|
+
left: 0; top: 8px; bottom: 8px;
|
|
324
|
+
width: 3px;
|
|
325
|
+
border-radius: 0 3px 3px 0;
|
|
326
|
+
background: var(--accent);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.sidebar-item svg {
|
|
330
|
+
width: 20px; height: 20px;
|
|
331
|
+
flex-shrink: 0;
|
|
332
|
+
opacity: 0.6;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.sidebar-item.active svg { opacity: 1; }
|
|
336
|
+
.sidebar-item:hover svg { opacity: 0.85; }
|
|
337
|
+
|
|
338
|
+
.sidebar-item .item-badge {
|
|
339
|
+
margin-left: auto;
|
|
340
|
+
font-size: 11px;
|
|
341
|
+
font-weight: 600;
|
|
342
|
+
padding: 2px 8px;
|
|
343
|
+
border-radius: 999px;
|
|
344
|
+
background: var(--warning-soft);
|
|
345
|
+
color: var(--warning);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.sidebar-spacer { flex: 1; }
|
|
349
|
+
|
|
350
|
+
.sidebar-footer {
|
|
351
|
+
padding: 16px 12px;
|
|
352
|
+
border-top: 1px solid var(--border-light);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.sidebar-footer-item {
|
|
356
|
+
display: flex;
|
|
357
|
+
align-items: center;
|
|
358
|
+
gap: 12px;
|
|
359
|
+
padding: 10px 12px;
|
|
360
|
+
border-radius: var(--radius-sm);
|
|
361
|
+
cursor: pointer;
|
|
362
|
+
color: var(--text-muted);
|
|
363
|
+
font-size: 13px;
|
|
364
|
+
font-weight: 500;
|
|
365
|
+
transition: all 0.15s ease;
|
|
366
|
+
border: none;
|
|
367
|
+
background: transparent;
|
|
368
|
+
width: 100%;
|
|
369
|
+
text-align: left;
|
|
370
|
+
font-family: var(--sans);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.sidebar-footer-item:hover {
|
|
374
|
+
background: var(--sidebar-hover);
|
|
375
|
+
color: var(--text-dim);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.sidebar-footer-item.active {
|
|
379
|
+
background: var(--sidebar-active);
|
|
380
|
+
color: var(--accent);
|
|
381
|
+
font-weight: 600;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.sidebar-footer-item svg {
|
|
385
|
+
width: 18px; height: 18px;
|
|
386
|
+
flex-shrink: 0;
|
|
387
|
+
opacity: 0.5;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* ══════════════════════════════════════════════════
|
|
391
|
+
MAIN CONTENT
|
|
392
|
+
══════════════════════════════════════════════════ */
|
|
393
|
+
.main {
|
|
394
|
+
overflow-y: auto;
|
|
395
|
+
padding: 32px 36px;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* ── Page Header ──────────────────────────────────── */
|
|
399
|
+
.page-header {
|
|
400
|
+
display: flex;
|
|
401
|
+
justify-content: space-between;
|
|
402
|
+
align-items: center;
|
|
403
|
+
margin-bottom: 28px;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.page-title {
|
|
407
|
+
font-family: var(--serif);
|
|
408
|
+
font-size: 26px;
|
|
409
|
+
font-weight: 700;
|
|
410
|
+
letter-spacing: -0.02em;
|
|
411
|
+
color: var(--text);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.header-actions { display: flex; gap: 10px; align-items: center; }
|
|
415
|
+
|
|
416
|
+
.header-meta {
|
|
417
|
+
font-size: 12px;
|
|
418
|
+
color: var(--text-muted);
|
|
419
|
+
font-family: var(--mono);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.theme-toggle {
|
|
423
|
+
width: 36px; height: 36px;
|
|
424
|
+
border: 1px solid var(--border);
|
|
425
|
+
background: var(--surface);
|
|
426
|
+
border-radius: var(--radius-sm);
|
|
427
|
+
cursor: pointer;
|
|
428
|
+
font-size: 16px;
|
|
429
|
+
display: flex; align-items: center; justify-content: center;
|
|
430
|
+
transition: all 0.15s ease;
|
|
431
|
+
color: var(--text);
|
|
432
|
+
}
|
|
433
|
+
.theme-toggle:hover { background: var(--surface-hover); }
|
|
434
|
+
|
|
435
|
+
/* ── Pages ─────────────────────────────────────────── */
|
|
436
|
+
.page { display: none; }
|
|
437
|
+
.page.active { display: block; }
|
|
438
|
+
|
|
439
|
+
/* ══════════════════════════════════════════════════
|
|
440
|
+
WELCOME BANNER
|
|
441
|
+
══════════════════════════════════════════════════ */
|
|
442
|
+
.welcome-banner {
|
|
443
|
+
background: linear-gradient(135deg, var(--welcome-from), var(--welcome-to));
|
|
444
|
+
border-radius: var(--radius);
|
|
445
|
+
padding: 28px 32px;
|
|
446
|
+
margin-bottom: 28px;
|
|
447
|
+
color: var(--welcome-text);
|
|
448
|
+
position: relative;
|
|
449
|
+
overflow: hidden;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.welcome-banner::after {
|
|
453
|
+
content: '';
|
|
454
|
+
position: absolute;
|
|
455
|
+
right: -20px; top: -20px;
|
|
456
|
+
width: 200px; height: 200px;
|
|
457
|
+
border-radius: 50%;
|
|
458
|
+
background: rgba(255,255,255,0.08);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.welcome-banner::before {
|
|
462
|
+
content: '';
|
|
463
|
+
position: absolute;
|
|
464
|
+
right: 80px; bottom: -40px;
|
|
465
|
+
width: 120px; height: 120px;
|
|
466
|
+
border-radius: 50%;
|
|
467
|
+
background: rgba(255,255,255,0.05);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.welcome-greeting {
|
|
471
|
+
font-family: var(--serif);
|
|
472
|
+
font-size: 24px;
|
|
473
|
+
font-weight: 700;
|
|
474
|
+
margin-bottom: 6px;
|
|
475
|
+
letter-spacing: -0.01em;
|
|
476
|
+
position: relative; z-index: 1;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.welcome-sub {
|
|
480
|
+
font-size: 14px;
|
|
481
|
+
opacity: 0.85;
|
|
482
|
+
font-weight: 400;
|
|
483
|
+
position: relative; z-index: 1;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* ══════════════════════════════════════════════════
|
|
487
|
+
APPROVAL BANNER
|
|
488
|
+
══════════════════════════════════════════════════ */
|
|
489
|
+
.approval-banner {
|
|
490
|
+
background: var(--warning-soft);
|
|
491
|
+
border: 1px solid rgba(245, 158, 11, 0.15);
|
|
492
|
+
border-radius: var(--radius);
|
|
493
|
+
padding: 18px 22px;
|
|
494
|
+
margin-bottom: 24px;
|
|
495
|
+
display: flex;
|
|
496
|
+
align-items: center;
|
|
497
|
+
gap: 16px;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.approval-banner-icon { font-size: 22px; flex-shrink: 0; }
|
|
501
|
+
.approval-banner-content { flex: 1; }
|
|
502
|
+
.approval-banner-title { font-weight: 600; font-size: 14px; margin-bottom: 3px; }
|
|
503
|
+
.approval-banner-detail { font-size: 13px; color: var(--text-dim); font-family: var(--mono); }
|
|
504
|
+
.approval-banner-actions { display: flex; gap: 8px; }
|
|
505
|
+
|
|
506
|
+
/* ══════════════════════════════════════════════════
|
|
507
|
+
STAT CARDS
|
|
508
|
+
══════════════════════════════════════════════════ */
|
|
509
|
+
.stats-grid {
|
|
510
|
+
display: grid;
|
|
511
|
+
grid-template-columns: repeat(4, 1fr);
|
|
512
|
+
gap: 18px;
|
|
513
|
+
margin-bottom: 28px;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.stat-card {
|
|
517
|
+
background: var(--surface);
|
|
518
|
+
border-radius: var(--radius);
|
|
519
|
+
padding: 22px 24px;
|
|
520
|
+
box-shadow: var(--shadow-sm);
|
|
521
|
+
transition: all 0.2s ease;
|
|
522
|
+
border: 1px solid var(--border-light);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.stat-card:hover {
|
|
526
|
+
box-shadow: var(--shadow);
|
|
527
|
+
transform: translateY(-1px);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.stat-icon {
|
|
531
|
+
width: 40px; height: 40px;
|
|
532
|
+
border-radius: var(--radius-sm);
|
|
533
|
+
display: flex; align-items: center; justify-content: center;
|
|
534
|
+
font-size: 18px;
|
|
535
|
+
margin-bottom: 14px;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.stat-icon.sage { background: var(--accent-soft); color: var(--accent); }
|
|
539
|
+
.stat-icon.green { background: var(--success-soft); color: var(--success); }
|
|
540
|
+
.stat-icon.amber { background: var(--warning-soft); color: var(--warning); }
|
|
541
|
+
.stat-icon.blue { background: rgba(86, 119, 145, 0.08); color: #567791; }
|
|
542
|
+
|
|
543
|
+
.stat-label {
|
|
544
|
+
font-size: 12px;
|
|
545
|
+
color: var(--text-muted);
|
|
546
|
+
font-weight: 500;
|
|
547
|
+
margin-bottom: 6px;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.stat-value {
|
|
551
|
+
font-family: var(--serif);
|
|
552
|
+
font-size: 28px;
|
|
553
|
+
font-weight: 700;
|
|
554
|
+
line-height: 1.1;
|
|
555
|
+
letter-spacing: -0.02em;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.stat-sub {
|
|
559
|
+
font-size: 12px;
|
|
560
|
+
color: var(--text-muted);
|
|
561
|
+
margin-top: 4px;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/* ══════════════════════════════════════════════════
|
|
565
|
+
SECTIONS
|
|
566
|
+
══════════════════════════════════════════════════ */
|
|
567
|
+
.section { margin-bottom: 28px; }
|
|
568
|
+
|
|
569
|
+
.section-header {
|
|
570
|
+
display: flex;
|
|
571
|
+
justify-content: space-between;
|
|
572
|
+
align-items: center;
|
|
573
|
+
margin-bottom: 16px;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.section-title {
|
|
577
|
+
font-family: var(--serif);
|
|
578
|
+
font-size: 18px;
|
|
579
|
+
font-weight: 700;
|
|
580
|
+
color: var(--text);
|
|
581
|
+
letter-spacing: -0.01em;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.section-link {
|
|
585
|
+
font-size: 13px;
|
|
586
|
+
color: var(--accent);
|
|
587
|
+
cursor: pointer;
|
|
588
|
+
text-decoration: none;
|
|
589
|
+
font-weight: 500;
|
|
590
|
+
}
|
|
591
|
+
.section-link:hover { text-decoration: underline; }
|
|
592
|
+
|
|
593
|
+
/* ══════════════════════════════════════════════════
|
|
594
|
+
ACTIVITY FEED
|
|
595
|
+
══════════════════════════════════════════════════ */
|
|
596
|
+
.feed-card {
|
|
597
|
+
background: var(--surface);
|
|
598
|
+
border-radius: var(--radius);
|
|
599
|
+
box-shadow: var(--shadow-sm);
|
|
600
|
+
border: 1px solid var(--border-light);
|
|
601
|
+
overflow: hidden;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.feed { display: flex; flex-direction: column; }
|
|
605
|
+
|
|
606
|
+
.feed-item {
|
|
607
|
+
display: grid;
|
|
608
|
+
grid-template-columns: 40px 1fr auto;
|
|
609
|
+
gap: 14px;
|
|
610
|
+
align-items: start;
|
|
611
|
+
padding: 16px 20px;
|
|
612
|
+
transition: background 0.12s ease;
|
|
613
|
+
border-bottom: 1px solid var(--border-light);
|
|
614
|
+
}
|
|
615
|
+
.feed-item:last-child { border-bottom: none; }
|
|
616
|
+
.feed-item:hover { background: var(--surface-alt); }
|
|
617
|
+
|
|
618
|
+
.feed-icon {
|
|
619
|
+
width: 40px; height: 40px;
|
|
620
|
+
border-radius: var(--radius-sm);
|
|
621
|
+
display: flex; align-items: center; justify-content: center;
|
|
622
|
+
font-size: 16px;
|
|
623
|
+
flex-shrink: 0;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.feed-icon.telegram { background: var(--accent-soft); color: var(--accent); }
|
|
627
|
+
.feed-icon.cron { background: var(--warning-soft); color: var(--warning); }
|
|
628
|
+
.feed-icon.system { background: var(--surface-alt); color: var(--text-muted); }
|
|
629
|
+
.feed-icon.code { background: var(--accent-soft); color: var(--accent); }
|
|
630
|
+
|
|
631
|
+
.feed-title { font-weight: 600; font-size: 14px; margin-bottom: 3px; }
|
|
632
|
+
.feed-detail { font-size: 13px; color: var(--text-dim); line-height: 1.45; }
|
|
633
|
+
.feed-time { font-size: 11px; color: var(--text-muted); font-family: var(--mono); white-space: nowrap; padding-top: 2px; }
|
|
634
|
+
|
|
635
|
+
/* ══════════════════════════════════════════════════
|
|
636
|
+
CRON CARDS
|
|
637
|
+
══════════════════════════════════════════════════ */
|
|
638
|
+
.cron-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
|
639
|
+
|
|
640
|
+
.cron-card {
|
|
641
|
+
background: var(--surface);
|
|
642
|
+
border: 1px solid var(--border-light);
|
|
643
|
+
border-radius: var(--radius);
|
|
644
|
+
padding: 20px 22px;
|
|
645
|
+
display: flex;
|
|
646
|
+
justify-content: space-between;
|
|
647
|
+
align-items: center;
|
|
648
|
+
box-shadow: var(--shadow-sm);
|
|
649
|
+
transition: all 0.15s ease;
|
|
650
|
+
}
|
|
651
|
+
.cron-card:hover { box-shadow: var(--shadow); transform: translateY(-1px); }
|
|
652
|
+
|
|
653
|
+
.cron-info { flex: 1; }
|
|
654
|
+
.cron-name { font-weight: 600; font-size: 14px; margin-bottom: 4px; }
|
|
655
|
+
.cron-schedule { font-size: 12px; color: var(--text-muted); font-family: var(--mono); }
|
|
656
|
+
.cron-next { font-size: 12px; color: var(--success); font-family: var(--mono); text-align: right; font-weight: 500; }
|
|
657
|
+
|
|
658
|
+
/* ══════════════════════════════════════════════════
|
|
659
|
+
BUTTONS
|
|
660
|
+
══════════════════════════════════════════════════ */
|
|
661
|
+
.btn {
|
|
662
|
+
padding: 8px 16px;
|
|
663
|
+
border: 1px solid var(--border);
|
|
664
|
+
border-radius: var(--radius-sm);
|
|
665
|
+
background: var(--surface);
|
|
666
|
+
color: var(--text);
|
|
667
|
+
font-family: var(--sans);
|
|
668
|
+
font-weight: 500;
|
|
669
|
+
font-size: 13px;
|
|
670
|
+
cursor: pointer;
|
|
671
|
+
transition: all 0.12s ease;
|
|
672
|
+
}
|
|
673
|
+
.btn:hover { background: var(--surface-hover); }
|
|
674
|
+
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
675
|
+
.btn-primary:hover { background: var(--accent-hover); }
|
|
676
|
+
.btn-success { background: var(--success); border-color: var(--success); color: #fff; }
|
|
677
|
+
.btn-success:hover { opacity: 0.9; }
|
|
678
|
+
.btn-danger { background: var(--error-soft); border-color: rgba(239, 68, 68, 0.2); color: var(--error); }
|
|
679
|
+
.btn-danger:hover { background: rgba(239, 68, 68, 0.12); }
|
|
680
|
+
.btn-sm { padding: 5px 12px; font-size: 12px; }
|
|
681
|
+
.btn-small { padding: 5px 12px; font-size: 12px; }
|
|
682
|
+
|
|
683
|
+
/* ══════════════════════════════════════════════════
|
|
684
|
+
HEALTH CHECKS
|
|
685
|
+
══════════════════════════════════════════════════ */
|
|
686
|
+
.health-card {
|
|
687
|
+
background: var(--surface);
|
|
688
|
+
border-radius: var(--radius);
|
|
689
|
+
box-shadow: var(--shadow-sm);
|
|
690
|
+
border: 1px solid var(--border-light);
|
|
691
|
+
overflow: hidden;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.health-grid { display: flex; flex-direction: column; }
|
|
695
|
+
|
|
696
|
+
.health-row {
|
|
697
|
+
display: grid;
|
|
698
|
+
grid-template-columns: 28px 1fr auto;
|
|
699
|
+
gap: 14px;
|
|
700
|
+
align-items: center;
|
|
701
|
+
padding: 14px 20px;
|
|
702
|
+
border-bottom: 1px solid var(--border-light);
|
|
703
|
+
transition: background 0.12s ease;
|
|
704
|
+
}
|
|
705
|
+
.health-row:last-child { border-bottom: none; }
|
|
706
|
+
.health-row:hover { background: var(--surface-alt); }
|
|
707
|
+
|
|
708
|
+
.health-dot {
|
|
709
|
+
width: 10px; height: 10px;
|
|
710
|
+
border-radius: 50%;
|
|
711
|
+
}
|
|
712
|
+
.health-dot.pass { background: var(--success); }
|
|
713
|
+
.health-dot.fail { background: var(--error); }
|
|
714
|
+
.health-dot.warn { background: var(--warning); }
|
|
715
|
+
|
|
716
|
+
.health-name { font-weight: 500; font-size: 14px; }
|
|
717
|
+
.health-value { font-size: 12px; font-family: var(--mono); color: var(--text-muted); }
|
|
718
|
+
|
|
719
|
+
/* ══════════════════════════════════════════════════
|
|
720
|
+
SPLIT VIEW (History, Digests, etc.)
|
|
721
|
+
══════════════════════════════════════════════════ */
|
|
722
|
+
.split-view {
|
|
723
|
+
display: grid;
|
|
724
|
+
grid-template-columns: 320px 1fr;
|
|
725
|
+
gap: 18px;
|
|
726
|
+
height: calc(100vh - 160px);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.split, .split-view {
|
|
730
|
+
display: grid;
|
|
731
|
+
grid-template-columns: 320px 1fr;
|
|
732
|
+
gap: 18px;
|
|
733
|
+
height: calc(100vh - 160px);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.split-left, .split-list {
|
|
737
|
+
background: var(--surface);
|
|
738
|
+
border: 1px solid var(--border-light);
|
|
739
|
+
border-radius: var(--radius);
|
|
740
|
+
overflow-y: auto;
|
|
741
|
+
box-shadow: var(--shadow-sm);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.split-right, .split-detail {
|
|
745
|
+
background: var(--surface);
|
|
746
|
+
border: 1px solid var(--border-light);
|
|
747
|
+
border-radius: var(--radius);
|
|
748
|
+
overflow-y: auto;
|
|
749
|
+
padding: 24px;
|
|
750
|
+
box-shadow: var(--shadow-sm);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.list-item {
|
|
754
|
+
padding: 16px 20px;
|
|
755
|
+
border-bottom: 1px solid var(--border-light);
|
|
756
|
+
cursor: pointer;
|
|
757
|
+
transition: background 0.12s ease;
|
|
758
|
+
}
|
|
759
|
+
.list-item:last-child { border-bottom: none; }
|
|
760
|
+
.list-item:hover { background: var(--surface-alt); }
|
|
761
|
+
.list-item.active { background: var(--accent-soft-2); border-left: 3px solid var(--accent); }
|
|
762
|
+
.list-item-title { font-weight: 600; font-size: 14px; margin-bottom: 3px; }
|
|
763
|
+
.list-item-sub, .list-item-meta { font-size: 12px; color: var(--text-muted); font-family: var(--mono); }
|
|
764
|
+
|
|
765
|
+
/* ── Chat Bubbles ─────────────────────────────────── */
|
|
766
|
+
.chat-msg { margin-bottom: 16px; max-width: 80%; }
|
|
767
|
+
.chat-msg.user { margin-left: auto; }
|
|
768
|
+
.chat-role {
|
|
769
|
+
font-size: 10px; color: var(--text-muted);
|
|
770
|
+
text-transform: uppercase; letter-spacing: 0.06em;
|
|
771
|
+
font-family: var(--mono); margin-bottom: 6px; font-weight: 600;
|
|
772
|
+
}
|
|
773
|
+
.chat-bubble {
|
|
774
|
+
padding: 14px 18px;
|
|
775
|
+
border-radius: var(--radius-md);
|
|
776
|
+
font-size: 14px; line-height: 1.6;
|
|
777
|
+
white-space: pre-wrap;
|
|
778
|
+
word-break: break-word;
|
|
779
|
+
}
|
|
780
|
+
.chat-msg.user .chat-bubble, .chat-bubble.user { background: var(--accent-soft); border: 1px solid rgba(74, 124, 111, 0.12); margin-left: auto; }
|
|
781
|
+
.chat-msg.assistant .chat-bubble, .chat-bubble.assistant { background: var(--surface-alt); border: 1px solid var(--border); }
|
|
782
|
+
|
|
783
|
+
/* ══════════════════════════════════════════════════
|
|
784
|
+
LOG VIEWER
|
|
785
|
+
══════════════════════════════════════════════════ */
|
|
786
|
+
.log-pane, .log-viewer {
|
|
787
|
+
background: var(--surface);
|
|
788
|
+
border: 1px solid var(--border-light);
|
|
789
|
+
border-radius: var(--radius);
|
|
790
|
+
font-family: var(--mono);
|
|
791
|
+
font-size: 12px; line-height: 1.8;
|
|
792
|
+
padding: 18px 20px;
|
|
793
|
+
white-space: pre-wrap; word-break: break-all;
|
|
794
|
+
max-height: calc(100vh - 260px);
|
|
795
|
+
overflow-y: auto;
|
|
796
|
+
box-shadow: var(--shadow-sm);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.log-line-info { color: var(--log-info); }
|
|
800
|
+
.log-line-warn { color: var(--log-warn); }
|
|
801
|
+
.log-line-error { color: var(--log-error); font-weight: 500; }
|
|
802
|
+
|
|
803
|
+
/* ══════════════════════════════════════════════════
|
|
804
|
+
EMPTY STATE
|
|
805
|
+
══════════════════════════════════════════════════ */
|
|
806
|
+
.empty-state, .empty {
|
|
807
|
+
text-align: center;
|
|
808
|
+
padding: 64px 24px;
|
|
809
|
+
color: var(--text-muted);
|
|
810
|
+
}
|
|
811
|
+
.empty-state-icon { font-size: 40px; margin-bottom: 16px; opacity: 0.4; }
|
|
812
|
+
.empty-state-text { font-size: 14px; }
|
|
813
|
+
|
|
814
|
+
/* ══════════════════════════════════════════════════
|
|
815
|
+
CODING AGENT TREE (redesign)
|
|
816
|
+
══════════════════════════════════════════════════ */
|
|
817
|
+
.tree-container { display: flex; flex-direction: column; gap: 20px; }
|
|
818
|
+
|
|
819
|
+
.tree-card {
|
|
820
|
+
background: var(--surface);
|
|
821
|
+
border: 1px solid var(--border-light);
|
|
822
|
+
border-radius: var(--radius);
|
|
823
|
+
overflow: hidden;
|
|
824
|
+
box-shadow: var(--shadow-sm);
|
|
825
|
+
transition: all 0.15s ease;
|
|
826
|
+
}
|
|
827
|
+
.tree-card:hover { box-shadow: var(--shadow); }
|
|
828
|
+
|
|
829
|
+
.tree-card-header {
|
|
830
|
+
padding: 20px 24px;
|
|
831
|
+
display: flex; justify-content: space-between; align-items: start;
|
|
832
|
+
gap: 16px;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.tree-card-left { display: flex; flex-direction: column; gap: 8px; flex: 1; }
|
|
836
|
+
|
|
837
|
+
.tree-card-id {
|
|
838
|
+
font-family: var(--mono); font-size: 13px;
|
|
839
|
+
color: var(--accent); font-weight: 600;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.tree-card-task { font-size: 14px; line-height: 1.55; color: var(--text); }
|
|
843
|
+
|
|
844
|
+
.tree-card-meta {
|
|
845
|
+
display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
|
|
846
|
+
font-size: 12px; font-family: var(--mono); color: var(--text-muted);
|
|
847
|
+
margin-top: 2px;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.tree-card-right { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; flex-shrink: 0; }
|
|
851
|
+
|
|
852
|
+
.badge-status {
|
|
853
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
854
|
+
padding: 5px 14px; border-radius: 999px;
|
|
855
|
+
font-size: 11px; font-weight: 600;
|
|
856
|
+
text-transform: uppercase; letter-spacing: 0.04em;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
.badge-status.running { background: var(--accent-soft); color: var(--accent); }
|
|
860
|
+
.badge-status.completed { background: var(--success-soft); color: var(--success); }
|
|
861
|
+
.badge-status.failed { background: var(--error-soft); color: var(--error); }
|
|
862
|
+
.badge-status.validating { background: var(--warning-soft); color: var(--warning); }
|
|
863
|
+
|
|
864
|
+
.badge-team {
|
|
865
|
+
padding: 4px 12px; border-radius: 999px;
|
|
866
|
+
font-size: 11px; font-weight: 600;
|
|
867
|
+
background: var(--accent-soft); color: var(--accent);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
871
|
+
|
|
872
|
+
.spinner {
|
|
873
|
+
display: inline-block; width: 10px; height: 10px;
|
|
874
|
+
border: 2px solid currentColor; border-top-color: transparent;
|
|
875
|
+
border-radius: 50%; animation: spin 0.8s linear infinite;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.tree-progress { height: 3px; background: var(--surface-alt); }
|
|
879
|
+
|
|
880
|
+
.tree-progress-bar {
|
|
881
|
+
height: 100%; border-radius: 0 0 2px 2px;
|
|
882
|
+
transition: width 0.3s ease;
|
|
883
|
+
}
|
|
884
|
+
.tree-progress-bar.running { background: var(--accent); }
|
|
885
|
+
.tree-progress-bar.completed { background: var(--success); }
|
|
886
|
+
.tree-progress-bar.failed { background: var(--error); }
|
|
887
|
+
|
|
888
|
+
.tree-children {
|
|
889
|
+
margin: 0 24px 20px 24px;
|
|
890
|
+
padding-left: 20px;
|
|
891
|
+
border-left: 2px solid var(--border);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.tree-children-label {
|
|
895
|
+
font-size: 12px; font-weight: 600; color: var(--text-muted);
|
|
896
|
+
text-transform: uppercase; letter-spacing: 0.04em;
|
|
897
|
+
padding: 14px 0 10px 0;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.tree-child-card {
|
|
901
|
+
background: var(--surface-alt);
|
|
902
|
+
border: 1px solid var(--border);
|
|
903
|
+
border-radius: var(--radius-sm);
|
|
904
|
+
margin-bottom: 10px;
|
|
905
|
+
overflow: hidden;
|
|
906
|
+
cursor: pointer;
|
|
907
|
+
transition: all 0.12s ease;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
.tree-child-card:hover { background: var(--surface-hover); }
|
|
911
|
+
|
|
912
|
+
.tree-child-card.status-completed { border-left: 3px solid var(--success); }
|
|
913
|
+
.tree-child-card.status-running { border-left: 3px solid var(--accent); }
|
|
914
|
+
.tree-child-card.status-failed { border-left: 3px solid var(--error); }
|
|
915
|
+
.tree-child-card.status-validating { border-left: 3px solid var(--warning); }
|
|
916
|
+
|
|
917
|
+
.tree-child-header {
|
|
918
|
+
padding: 14px 16px;
|
|
919
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
920
|
+
gap: 12px;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.tree-child-left { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
|
|
924
|
+
.tree-child-icon { font-size: 16px; flex-shrink: 0; }
|
|
925
|
+
.tree-child-info { flex: 1; min-width: 0; }
|
|
926
|
+
.tree-child-id { font-family: var(--mono); font-size: 12px; color: var(--accent); font-weight: 500; }
|
|
927
|
+
.tree-child-task { font-size: 13px; color: var(--text); line-height: 1.45; margin-top: 3px; }
|
|
928
|
+
.tree-child-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
|
|
929
|
+
.tree-child-elapsed { font-size: 11px; font-family: var(--mono); color: var(--text-muted); }
|
|
930
|
+
|
|
931
|
+
.tree-child-detail {
|
|
932
|
+
display: none;
|
|
933
|
+
border-top: 1px solid var(--border);
|
|
934
|
+
padding: 14px 16px;
|
|
935
|
+
}
|
|
936
|
+
.tree-child-detail.expanded { display: block; }
|
|
937
|
+
|
|
938
|
+
.tree-output {
|
|
939
|
+
background: var(--surface);
|
|
940
|
+
border: 1px solid var(--border);
|
|
941
|
+
border-radius: var(--radius-sm);
|
|
942
|
+
padding: 14px 16px;
|
|
943
|
+
font-family: var(--mono); font-size: 12px; line-height: 1.7;
|
|
944
|
+
white-space: pre-wrap; word-break: break-word;
|
|
945
|
+
max-height: 300px; overflow-y: auto;
|
|
946
|
+
color: var(--text-dim);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
.tree-output.error {
|
|
950
|
+
background: var(--error-soft);
|
|
951
|
+
border-color: rgba(239, 68, 68, 0.15);
|
|
952
|
+
color: var(--error);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.tree-synthesis {
|
|
956
|
+
margin: 0 24px 20px 24px;
|
|
957
|
+
padding: 16px 20px;
|
|
958
|
+
background: var(--surface-alt);
|
|
959
|
+
border: 1px solid var(--border);
|
|
960
|
+
border-radius: var(--radius-sm);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.tree-synthesis-label {
|
|
964
|
+
font-size: 11px; font-weight: 600; color: var(--text-muted);
|
|
965
|
+
text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 8px;
|
|
966
|
+
}
|
|
967
|
+
.tree-synthesis-content { font-size: 14px; line-height: 1.6; color: var(--text); }
|
|
968
|
+
|
|
969
|
+
.detail-toggle {
|
|
970
|
+
font-size: 12px; color: var(--accent); font-weight: 500;
|
|
971
|
+
font-family: var(--mono); cursor: pointer;
|
|
972
|
+
padding: 10px 24px; user-select: none;
|
|
973
|
+
}
|
|
974
|
+
.detail-toggle:hover { text-decoration: underline; }
|
|
975
|
+
|
|
976
|
+
.tree-summary {
|
|
977
|
+
display: flex; gap: 14px; margin-bottom: 24px; flex-wrap: wrap;
|
|
978
|
+
}
|
|
979
|
+
.tree-summary-pill {
|
|
980
|
+
display: flex; align-items: center; gap: 8px;
|
|
981
|
+
padding: 10px 18px;
|
|
982
|
+
background: var(--surface);
|
|
983
|
+
border: 1px solid var(--border-light);
|
|
984
|
+
border-radius: var(--radius);
|
|
985
|
+
font-size: 13px; font-weight: 500;
|
|
986
|
+
box-shadow: var(--shadow-sm);
|
|
987
|
+
}
|
|
988
|
+
.tree-summary-pill .pill-num {
|
|
989
|
+
font-family: var(--serif);
|
|
990
|
+
font-size: 20px; font-weight: 700;
|
|
991
|
+
}
|
|
992
|
+
.tree-summary-pill .pill-label { color: var(--text-muted); font-size: 12px; }
|
|
993
|
+
.tree-summary-pill.running .pill-num { color: var(--accent); }
|
|
994
|
+
.tree-summary-pill.completed .pill-num { color: var(--success); }
|
|
995
|
+
.tree-summary-pill.failed .pill-num { color: var(--error); }
|
|
996
|
+
.tree-summary-pill.cost .pill-num { color: var(--text); }
|
|
997
|
+
|
|
998
|
+
.cost-badge {
|
|
999
|
+
background: var(--surface-alt);
|
|
1000
|
+
padding: 2px 8px;
|
|
1001
|
+
border-radius: 999px;
|
|
1002
|
+
font-size: 11px;
|
|
1003
|
+
color: var(--text-dim);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
.tree-actions {
|
|
1007
|
+
display: flex; gap: 6px; margin-top: 4px;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.waterfall {
|
|
1011
|
+
background: var(--surface);
|
|
1012
|
+
border: 1px solid var(--border-light);
|
|
1013
|
+
border-radius: var(--radius);
|
|
1014
|
+
padding: 20px 24px;
|
|
1015
|
+
box-shadow: var(--shadow-sm);
|
|
1016
|
+
margin-bottom: 24px;
|
|
1017
|
+
}
|
|
1018
|
+
.waterfall-title {
|
|
1019
|
+
font-family: var(--serif);
|
|
1020
|
+
font-size: 16px; font-weight: 700;
|
|
1021
|
+
margin-bottom: 16px;
|
|
1022
|
+
}
|
|
1023
|
+
.waterfall-axis {
|
|
1024
|
+
display: flex; justify-content: space-between;
|
|
1025
|
+
font-size: 10px; font-family: var(--mono);
|
|
1026
|
+
color: var(--text-muted);
|
|
1027
|
+
margin-bottom: 10px;
|
|
1028
|
+
padding: 0 4px;
|
|
1029
|
+
}
|
|
1030
|
+
.waterfall-rows { display: flex; flex-direction: column; gap: 8px; }
|
|
1031
|
+
.waterfall-row {
|
|
1032
|
+
display: grid;
|
|
1033
|
+
grid-template-columns: 120px 1fr;
|
|
1034
|
+
gap: 12px;
|
|
1035
|
+
align-items: center;
|
|
1036
|
+
font-size: 12px;
|
|
1037
|
+
}
|
|
1038
|
+
.waterfall-label {
|
|
1039
|
+
font-family: var(--mono);
|
|
1040
|
+
color: var(--text-dim);
|
|
1041
|
+
font-size: 11px;
|
|
1042
|
+
text-align: right;
|
|
1043
|
+
overflow: hidden;
|
|
1044
|
+
text-overflow: ellipsis;
|
|
1045
|
+
white-space: nowrap;
|
|
1046
|
+
}
|
|
1047
|
+
.waterfall-track {
|
|
1048
|
+
height: 24px;
|
|
1049
|
+
background: var(--surface-alt);
|
|
1050
|
+
border-radius: 4px;
|
|
1051
|
+
position: relative;
|
|
1052
|
+
overflow: hidden;
|
|
1053
|
+
}
|
|
1054
|
+
.waterfall-bar {
|
|
1055
|
+
position: absolute;
|
|
1056
|
+
top: 3px; bottom: 3px;
|
|
1057
|
+
border-radius: 3px;
|
|
1058
|
+
min-width: 4px;
|
|
1059
|
+
}
|
|
1060
|
+
.waterfall-bar.completed { background: var(--success); opacity: 0.7; }
|
|
1061
|
+
.waterfall-bar.running { background: var(--accent); opacity: 0.7; animation: pulse-bar 1.5s ease infinite; }
|
|
1062
|
+
.waterfall-bar.failed { background: var(--error); opacity: 0.7; }
|
|
1063
|
+
@keyframes pulse-bar { 0%,100% { opacity: 0.5; } 50% { opacity: 0.9; } }
|
|
1064
|
+
|
|
1065
|
+
.file-changes {
|
|
1066
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
1067
|
+
font-size: 11px; font-family: var(--mono);
|
|
1068
|
+
color: var(--text-dim);
|
|
1069
|
+
background: var(--surface-alt);
|
|
1070
|
+
padding: 3px 10px;
|
|
1071
|
+
border-radius: 999px;
|
|
1072
|
+
}
|
|
1073
|
+
.file-changes .added { color: var(--success); }
|
|
1074
|
+
.file-changes .removed { color: var(--error); }
|
|
1075
|
+
|
|
1076
|
+
/* ══════════════════════════════════════════════════
|
|
1077
|
+
SKILLS
|
|
1078
|
+
══════════════════════════════════════════════════ */
|
|
1079
|
+
.skills-grid {
|
|
1080
|
+
display: grid;
|
|
1081
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
1082
|
+
gap: 16px;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.skill-card {
|
|
1086
|
+
background: var(--surface);
|
|
1087
|
+
border: 1px solid var(--border-light);
|
|
1088
|
+
border-radius: var(--radius);
|
|
1089
|
+
padding: 22px 24px;
|
|
1090
|
+
box-shadow: var(--shadow-sm);
|
|
1091
|
+
transition: all 0.15s ease;
|
|
1092
|
+
}
|
|
1093
|
+
.skill-card:hover { box-shadow: var(--shadow); transform: translateY(-1px); }
|
|
1094
|
+
|
|
1095
|
+
.skill-header { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
|
|
1096
|
+
.skill-emoji {
|
|
1097
|
+
width: 40px; height: 40px;
|
|
1098
|
+
border-radius: var(--radius-sm);
|
|
1099
|
+
background: var(--accent-soft);
|
|
1100
|
+
color: var(--accent);
|
|
1101
|
+
display: flex; align-items: center; justify-content: center;
|
|
1102
|
+
font-size: 13px;
|
|
1103
|
+
font-weight: 700;
|
|
1104
|
+
font-family: var(--mono);
|
|
1105
|
+
letter-spacing: -0.02em;
|
|
1106
|
+
}
|
|
1107
|
+
.skill-name { font-weight: 600; font-size: 15px; }
|
|
1108
|
+
.skill-desc { font-size: 13px; color: var(--text-dim); line-height: 1.5; margin-bottom: 12px; }
|
|
1109
|
+
.skill-tags { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
1110
|
+
.skill-tag {
|
|
1111
|
+
font-size: 11px; font-family: var(--mono);
|
|
1112
|
+
padding: 3px 10px; border-radius: 999px;
|
|
1113
|
+
background: var(--surface-alt); color: var(--text-muted);
|
|
1114
|
+
border: 1px solid var(--border-light);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/* ══════════════════════════════════════════════════
|
|
1118
|
+
CRON JOBS — ENHANCED
|
|
1119
|
+
══════════════════════════════════════════════════ */
|
|
1120
|
+
.cron-week-strip {
|
|
1121
|
+
background: var(--surface);
|
|
1122
|
+
border: 1px solid var(--border-light);
|
|
1123
|
+
border-radius: var(--radius);
|
|
1124
|
+
padding: 20px 24px;
|
|
1125
|
+
box-shadow: var(--shadow-sm);
|
|
1126
|
+
margin-bottom: 24px;
|
|
1127
|
+
}
|
|
1128
|
+
.cron-week-title {
|
|
1129
|
+
font-family: var(--serif);
|
|
1130
|
+
font-size: 16px; font-weight: 700;
|
|
1131
|
+
margin-bottom: 14px;
|
|
1132
|
+
}
|
|
1133
|
+
.cron-week-grid {
|
|
1134
|
+
display: grid;
|
|
1135
|
+
grid-template-columns: repeat(7, 1fr);
|
|
1136
|
+
gap: 8px;
|
|
1137
|
+
}
|
|
1138
|
+
.cron-week-day { text-align: center; }
|
|
1139
|
+
.cron-day-label {
|
|
1140
|
+
font-size: 10px; font-weight: 600;
|
|
1141
|
+
color: var(--text-muted);
|
|
1142
|
+
text-transform: uppercase;
|
|
1143
|
+
letter-spacing: 0.04em;
|
|
1144
|
+
margin-bottom: 8px;
|
|
1145
|
+
}
|
|
1146
|
+
.cron-day-dots {
|
|
1147
|
+
display: flex;
|
|
1148
|
+
flex-direction: column;
|
|
1149
|
+
align-items: center;
|
|
1150
|
+
gap: 4px;
|
|
1151
|
+
min-height: 60px;
|
|
1152
|
+
}
|
|
1153
|
+
.cron-dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
1154
|
+
.cron-dot.morning { background: var(--accent); }
|
|
1155
|
+
.cron-dot.reading { background: var(--warning); }
|
|
1156
|
+
.cron-dot.exercise { background: #e07c5a; }
|
|
1157
|
+
.cron-dot.heartbeat { background: var(--text-muted); opacity: 0.3; }
|
|
1158
|
+
.cron-dot-time {
|
|
1159
|
+
font-size: 9px; font-family: var(--mono);
|
|
1160
|
+
color: var(--text-muted);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
.cron-week-legend {
|
|
1164
|
+
display: flex; gap: 16px; margin-top: 12px;
|
|
1165
|
+
padding-top: 12px;
|
|
1166
|
+
border-top: 1px solid var(--border-light);
|
|
1167
|
+
}
|
|
1168
|
+
.cron-legend-item {
|
|
1169
|
+
display: flex; align-items: center; gap: 6px;
|
|
1170
|
+
font-size: 11px; color: var(--text-muted);
|
|
1171
|
+
}
|
|
1172
|
+
.cron-legend-dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
1173
|
+
|
|
1174
|
+
.cron-cards { display: flex; flex-direction: column; gap: 14px; }
|
|
1175
|
+
|
|
1176
|
+
.cron-card-rich {
|
|
1177
|
+
background: var(--surface);
|
|
1178
|
+
border: 1px solid var(--border-light);
|
|
1179
|
+
border-radius: var(--radius);
|
|
1180
|
+
box-shadow: var(--shadow-sm);
|
|
1181
|
+
overflow: hidden;
|
|
1182
|
+
transition: all 0.15s;
|
|
1183
|
+
}
|
|
1184
|
+
.cron-card-rich:hover { box-shadow: var(--shadow); }
|
|
1185
|
+
|
|
1186
|
+
.cron-card-main {
|
|
1187
|
+
padding: 18px 22px;
|
|
1188
|
+
display: flex;
|
|
1189
|
+
justify-content: space-between;
|
|
1190
|
+
align-items: start;
|
|
1191
|
+
gap: 16px;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
.cron-card-left { flex: 1; }
|
|
1195
|
+
.cron-card-top {
|
|
1196
|
+
display: flex; align-items: center; gap: 12px;
|
|
1197
|
+
margin-bottom: 6px;
|
|
1198
|
+
}
|
|
1199
|
+
.cron-card-name { font-weight: 600; font-size: 15px; }
|
|
1200
|
+
.cron-card-type {
|
|
1201
|
+
font-size: 10px; font-weight: 600;
|
|
1202
|
+
padding: 2px 8px; border-radius: 999px;
|
|
1203
|
+
text-transform: uppercase; letter-spacing: 0.04em;
|
|
1204
|
+
background: var(--accent-soft); color: var(--accent);
|
|
1205
|
+
}
|
|
1206
|
+
.cron-card-schedule {
|
|
1207
|
+
font-size: 13px; color: var(--text-dim);
|
|
1208
|
+
margin-bottom: 4px;
|
|
1209
|
+
}
|
|
1210
|
+
.cron-card-expr {
|
|
1211
|
+
font-size: 11px; font-family: var(--mono);
|
|
1212
|
+
color: var(--text-muted);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
.cron-card-right {
|
|
1216
|
+
display: flex;
|
|
1217
|
+
align-items: center;
|
|
1218
|
+
gap: 10px;
|
|
1219
|
+
flex-shrink: 0;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/* ══════════════════════════════════════════════════
|
|
1223
|
+
FORMS
|
|
1224
|
+
══════════════════════════════════════════════════ */
|
|
1225
|
+
select,
|
|
1226
|
+
input,
|
|
1227
|
+
textarea {
|
|
1228
|
+
background: var(--surface);
|
|
1229
|
+
color: var(--text);
|
|
1230
|
+
border: 1px solid var(--border);
|
|
1231
|
+
border-radius: var(--radius-sm);
|
|
1232
|
+
padding: 10px 12px;
|
|
1233
|
+
font-family: var(--mono);
|
|
1234
|
+
font-size: 14px;
|
|
1235
|
+
width: 100%;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
select:focus,
|
|
1239
|
+
input:focus,
|
|
1240
|
+
textarea:focus {
|
|
1241
|
+
outline: none;
|
|
1242
|
+
border-color: var(--accent);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
textarea {
|
|
1246
|
+
resize: vertical;
|
|
1247
|
+
min-height: 300px;
|
|
1248
|
+
line-height: 1.55;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
label {
|
|
1252
|
+
display: block;
|
|
1253
|
+
font-size: 13px;
|
|
1254
|
+
color: var(--text-dim);
|
|
1255
|
+
margin-bottom: 4px;
|
|
1256
|
+
font-weight: 500;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
table {
|
|
1260
|
+
width: 100%;
|
|
1261
|
+
border-collapse: collapse;
|
|
1262
|
+
font-size: 14px;
|
|
1263
|
+
font-family: var(--mono);
|
|
1264
|
+
}
|
|
1265
|
+
td {
|
|
1266
|
+
padding: 7px 8px;
|
|
1267
|
+
border-bottom: 1px solid var(--border);
|
|
1268
|
+
}
|
|
1269
|
+
td:first-child { color: var(--accent); }
|
|
1270
|
+
|
|
1271
|
+
/* ══════════════════════════════════════════════════
|
|
1272
|
+
LEGACY CLASSES (used by JS-generated HTML)
|
|
1273
|
+
══════════════════════════════════════════════════ */
|
|
1274
|
+
|
|
1275
|
+
/* Toast notification */
|
|
1276
|
+
.toast {
|
|
1277
|
+
position: fixed;
|
|
1278
|
+
bottom: 20px;
|
|
1279
|
+
right: 20px;
|
|
1280
|
+
padding: 12px 18px;
|
|
1281
|
+
border-radius: var(--radius-sm);
|
|
1282
|
+
font-size: 13px;
|
|
1283
|
+
font-family: var(--mono);
|
|
1284
|
+
z-index: 1000;
|
|
1285
|
+
border: 1px solid var(--border-light);
|
|
1286
|
+
box-shadow: var(--shadow-md);
|
|
1287
|
+
}
|
|
1288
|
+
.toast.success { background: var(--success-soft); color: var(--text); border-color: rgba(74, 124, 111, 0.15); }
|
|
1289
|
+
.toast.error { background: var(--error-soft); color: var(--text); border-color: rgba(194, 84, 80, 0.15); }
|
|
1290
|
+
|
|
1291
|
+
/* Card (model, config, health pages) */
|
|
1292
|
+
.card {
|
|
1293
|
+
background: var(--surface);
|
|
1294
|
+
border: 1px solid var(--border-light);
|
|
1295
|
+
border-radius: var(--radius);
|
|
1296
|
+
padding: 20px 22px;
|
|
1297
|
+
margin-bottom: 14px;
|
|
1298
|
+
box-shadow: var(--shadow-sm);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
.card-title {
|
|
1302
|
+
font-size: 14px;
|
|
1303
|
+
color: var(--text-dim);
|
|
1304
|
+
margin-bottom: 8px;
|
|
1305
|
+
font-weight: 500;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
.card-value {
|
|
1309
|
+
font-family: var(--serif);
|
|
1310
|
+
font-size: 28px;
|
|
1311
|
+
font-weight: 700;
|
|
1312
|
+
line-height: 1.2;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/* Grid (status cards) */
|
|
1316
|
+
.grid {
|
|
1317
|
+
display: grid;
|
|
1318
|
+
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
|
1319
|
+
gap: 14px;
|
|
1320
|
+
margin-bottom: 18px;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/* Toolbar */
|
|
1324
|
+
.toolbar {
|
|
1325
|
+
display: flex;
|
|
1326
|
+
gap: 8px;
|
|
1327
|
+
align-items: center;
|
|
1328
|
+
margin-bottom: 12px;
|
|
1329
|
+
flex-wrap: wrap;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/* Markdown content */
|
|
1333
|
+
.markdown-content {
|
|
1334
|
+
font-size: 14px;
|
|
1335
|
+
line-height: 1.7;
|
|
1336
|
+
color: var(--text);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
.markdown-content h1,
|
|
1340
|
+
.markdown-content h2,
|
|
1341
|
+
.markdown-content h3,
|
|
1342
|
+
.markdown-content h4,
|
|
1343
|
+
.markdown-content h5,
|
|
1344
|
+
.markdown-content h6 {
|
|
1345
|
+
margin: 16px 0 8px;
|
|
1346
|
+
line-height: 1.3;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
.markdown-content p { margin: 10px 0; }
|
|
1350
|
+
.markdown-content ul,
|
|
1351
|
+
.markdown-content ol { margin: 8px 0 10px 22px; }
|
|
1352
|
+
.markdown-content li { margin: 4px 0; }
|
|
1353
|
+
.markdown-content hr {
|
|
1354
|
+
border: 0;
|
|
1355
|
+
border-top: 1px solid var(--border);
|
|
1356
|
+
margin: 14px 0;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
.markdown-content code {
|
|
1360
|
+
background: var(--surface-alt);
|
|
1361
|
+
border: 1px solid var(--border);
|
|
1362
|
+
border-radius: 6px;
|
|
1363
|
+
padding: 1px 6px;
|
|
1364
|
+
font-family: var(--mono);
|
|
1365
|
+
font-size: 12px;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
.markdown-content pre {
|
|
1369
|
+
background: var(--surface-alt);
|
|
1370
|
+
border: 1px solid var(--border);
|
|
1371
|
+
border-radius: var(--radius-sm);
|
|
1372
|
+
padding: 12px;
|
|
1373
|
+
overflow-x: auto;
|
|
1374
|
+
margin: 10px 0;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
.markdown-content pre code {
|
|
1378
|
+
background: transparent;
|
|
1379
|
+
border: 0;
|
|
1380
|
+
padding: 0;
|
|
1381
|
+
font-size: 13px;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
.markdown-content blockquote {
|
|
1385
|
+
border-left: 3px solid var(--accent);
|
|
1386
|
+
margin: 10px 0;
|
|
1387
|
+
padding: 2px 0 2px 10px;
|
|
1388
|
+
color: var(--text-dim);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
.markdown-content table {
|
|
1392
|
+
width: 100%;
|
|
1393
|
+
border-collapse: collapse;
|
|
1394
|
+
margin: 10px 0;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
.markdown-content a { color: var(--accent); }
|
|
1398
|
+
.markdown-content a:hover { text-decoration: underline; }
|
|
1399
|
+
|
|
1400
|
+
/* Toggle label */
|
|
1401
|
+
.toggle-label {
|
|
1402
|
+
display: flex;
|
|
1403
|
+
align-items: center;
|
|
1404
|
+
gap: 6px;
|
|
1405
|
+
font-size: 13px;
|
|
1406
|
+
color: var(--text-dim);
|
|
1407
|
+
cursor: pointer;
|
|
1408
|
+
}
|
|
1409
|
+
.toggle-label input { width: auto; }
|
|
1410
|
+
|
|
1411
|
+
/* Unsaved badge */
|
|
1412
|
+
.unsaved { color: var(--warning); font-size: 13px; margin-left: 8px; font-family: var(--mono); }
|
|
1413
|
+
|
|
1414
|
+
/* Approval classes */
|
|
1415
|
+
.approval-tier {
|
|
1416
|
+
display: inline-block;
|
|
1417
|
+
padding: 2px 8px;
|
|
1418
|
+
border-radius: 999px;
|
|
1419
|
+
font-size: 11px;
|
|
1420
|
+
font-weight: 600;
|
|
1421
|
+
}
|
|
1422
|
+
.approval-tier.tier-1 { background: var(--surface-alt); color: var(--text-dim); }
|
|
1423
|
+
.approval-tier.tier-2 { background: var(--warning-soft); color: var(--warning); }
|
|
1424
|
+
.approval-tier.tier-3 { background: var(--error-soft); color: var(--error); }
|
|
1425
|
+
|
|
1426
|
+
.approval-countdown {
|
|
1427
|
+
font-size: 12px;
|
|
1428
|
+
color: var(--text-dim);
|
|
1429
|
+
font-family: var(--mono);
|
|
1430
|
+
}
|
|
1431
|
+
.approval-countdown.urgent { color: var(--error); font-weight: 600; }
|
|
1432
|
+
|
|
1433
|
+
.approval-command {
|
|
1434
|
+
font-family: var(--mono);
|
|
1435
|
+
font-size: 13px;
|
|
1436
|
+
background: var(--surface-alt);
|
|
1437
|
+
padding: 8px 12px;
|
|
1438
|
+
border-radius: var(--radius-sm);
|
|
1439
|
+
margin: 6px 0;
|
|
1440
|
+
word-break: break-all;
|
|
1441
|
+
border: 1px solid var(--border-light);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
.approval-actions {
|
|
1445
|
+
display: flex;
|
|
1446
|
+
gap: 8px;
|
|
1447
|
+
margin-top: 8px;
|
|
1448
|
+
}
|
|
1449
|
+
.approval-actions .btn { font-size: 12px; padding: 4px 12px; }
|
|
1450
|
+
|
|
1451
|
+
.approval-status {
|
|
1452
|
+
display: inline-block;
|
|
1453
|
+
padding: 2px 8px;
|
|
1454
|
+
border-radius: 999px;
|
|
1455
|
+
font-size: 11px;
|
|
1456
|
+
font-weight: 600;
|
|
1457
|
+
text-transform: uppercase;
|
|
1458
|
+
letter-spacing: 0.04em;
|
|
1459
|
+
}
|
|
1460
|
+
.approval-status.pending { background: var(--warning-soft); color: var(--warning); }
|
|
1461
|
+
.approval-status.approved { background: var(--success-soft); color: var(--success); }
|
|
1462
|
+
.approval-status.denied { background: var(--error-soft); color: var(--error); }
|
|
1463
|
+
.approval-status.expired { background: var(--surface-alt); color: var(--text-dim); }
|
|
1464
|
+
|
|
1465
|
+
/* Audit classes */
|
|
1466
|
+
.audit-entry {
|
|
1467
|
+
background: var(--surface);
|
|
1468
|
+
border: 1px solid var(--border-light);
|
|
1469
|
+
border-radius: var(--radius);
|
|
1470
|
+
padding: 16px 20px;
|
|
1471
|
+
margin-bottom: 12px;
|
|
1472
|
+
transition: background 0.12s ease;
|
|
1473
|
+
box-shadow: var(--shadow-sm);
|
|
1474
|
+
}
|
|
1475
|
+
.audit-entry:hover { background: var(--surface-alt); }
|
|
1476
|
+
|
|
1477
|
+
.audit-header {
|
|
1478
|
+
display: flex;
|
|
1479
|
+
justify-content: space-between;
|
|
1480
|
+
align-items: center;
|
|
1481
|
+
margin-bottom: 6px;
|
|
1482
|
+
gap: 10px;
|
|
1483
|
+
flex-wrap: wrap;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
.audit-id {
|
|
1487
|
+
font-family: var(--mono);
|
|
1488
|
+
font-size: 13px;
|
|
1489
|
+
color: var(--accent);
|
|
1490
|
+
font-weight: 600;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
.audit-meta {
|
|
1494
|
+
display: flex;
|
|
1495
|
+
gap: 10px;
|
|
1496
|
+
align-items: center;
|
|
1497
|
+
font-size: 12px;
|
|
1498
|
+
color: var(--text-dim);
|
|
1499
|
+
font-family: var(--mono);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
.audit-badge {
|
|
1503
|
+
display: inline-block;
|
|
1504
|
+
padding: 2px 8px;
|
|
1505
|
+
border-radius: 999px;
|
|
1506
|
+
font-size: 11px;
|
|
1507
|
+
font-weight: 600;
|
|
1508
|
+
text-transform: uppercase;
|
|
1509
|
+
letter-spacing: 0.04em;
|
|
1510
|
+
}
|
|
1511
|
+
.audit-badge.ok { background: var(--success-soft); color: var(--success); }
|
|
1512
|
+
.audit-badge.error { background: var(--error-soft); color: var(--error); }
|
|
1513
|
+
|
|
1514
|
+
.audit-trigger {
|
|
1515
|
+
display: inline-block;
|
|
1516
|
+
padding: 2px 8px;
|
|
1517
|
+
border-radius: 999px;
|
|
1518
|
+
font-size: 11px;
|
|
1519
|
+
font-weight: 600;
|
|
1520
|
+
text-transform: uppercase;
|
|
1521
|
+
letter-spacing: 0.04em;
|
|
1522
|
+
}
|
|
1523
|
+
.audit-trigger.telegram { background: var(--accent-soft); color: var(--accent); }
|
|
1524
|
+
.audit-trigger.cron { background: var(--warning-soft); color: var(--warning); }
|
|
1525
|
+
.audit-trigger.api { background: var(--success-soft); color: var(--success); }
|
|
1526
|
+
.audit-trigger.system { background: var(--surface-alt); color: var(--text-dim); border: 1px solid var(--border); }
|
|
1527
|
+
.audit-trigger.discord { background: rgba(88, 101, 242, 0.1); color: #7289da; }
|
|
1528
|
+
.audit-trigger.code_agent { background: rgba(147, 51, 234, 0.1); color: #9333ea; }
|
|
1529
|
+
|
|
1530
|
+
.audit-summary {
|
|
1531
|
+
display: flex;
|
|
1532
|
+
gap: 12px;
|
|
1533
|
+
align-items: center;
|
|
1534
|
+
font-size: 13px;
|
|
1535
|
+
color: var(--text-dim);
|
|
1536
|
+
margin-bottom: 4px;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
.audit-events-toggle {
|
|
1540
|
+
font-size: 13px;
|
|
1541
|
+
color: var(--accent);
|
|
1542
|
+
cursor: pointer;
|
|
1543
|
+
user-select: none;
|
|
1544
|
+
margin-top: 6px;
|
|
1545
|
+
font-family: var(--mono);
|
|
1546
|
+
}
|
|
1547
|
+
.audit-events-toggle:hover { text-decoration: underline; }
|
|
1548
|
+
|
|
1549
|
+
.audit-events {
|
|
1550
|
+
display: none;
|
|
1551
|
+
margin-top: 8px;
|
|
1552
|
+
padding: 10px 0 0 0;
|
|
1553
|
+
border-top: 1px solid var(--border);
|
|
1554
|
+
}
|
|
1555
|
+
.audit-events.expanded { display: block; }
|
|
1556
|
+
|
|
1557
|
+
.audit-event {
|
|
1558
|
+
display: grid;
|
|
1559
|
+
grid-template-columns: 80px 1fr auto;
|
|
1560
|
+
gap: 8px;
|
|
1561
|
+
padding: 6px 0;
|
|
1562
|
+
font-size: 13px;
|
|
1563
|
+
border-bottom: 1px solid var(--border-light);
|
|
1564
|
+
align-items: center;
|
|
1565
|
+
}
|
|
1566
|
+
.audit-event:last-child { border-bottom: none; }
|
|
1567
|
+
|
|
1568
|
+
.audit-event-type {
|
|
1569
|
+
font-family: var(--mono);
|
|
1570
|
+
font-size: 12px;
|
|
1571
|
+
color: var(--accent);
|
|
1572
|
+
font-weight: 500;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
.audit-event-summary {
|
|
1576
|
+
color: var(--text);
|
|
1577
|
+
line-height: 1.4;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
.audit-event-duration {
|
|
1581
|
+
font-family: var(--mono);
|
|
1582
|
+
font-size: 12px;
|
|
1583
|
+
color: var(--text-dim);
|
|
1584
|
+
text-align: right;
|
|
1585
|
+
white-space: nowrap;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
/* Coding Agent legacy classes */
|
|
1589
|
+
.ca-status-badge {
|
|
1590
|
+
display: inline-flex;
|
|
1591
|
+
align-items: center;
|
|
1592
|
+
gap: 6px;
|
|
1593
|
+
padding: 4px 12px;
|
|
1594
|
+
border-radius: 999px;
|
|
1595
|
+
font-size: 13px;
|
|
1596
|
+
font-weight: 600;
|
|
1597
|
+
text-transform: uppercase;
|
|
1598
|
+
letter-spacing: 0.04em;
|
|
1599
|
+
}
|
|
1600
|
+
.ca-status-badge.idle { background: var(--surface-alt); color: var(--text-dim); }
|
|
1601
|
+
.ca-status-badge.running { background: var(--accent-soft); color: var(--accent); }
|
|
1602
|
+
.ca-status-badge.validating { background: var(--warning-soft); color: var(--warning); }
|
|
1603
|
+
.ca-status-badge.completed { background: var(--success-soft); color: var(--success); }
|
|
1604
|
+
.ca-status-badge.failed { background: var(--error-soft); color: var(--error); }
|
|
1605
|
+
.ca-status-badge.timeout { background: var(--error-soft); color: var(--error); }
|
|
1606
|
+
|
|
1607
|
+
.ca-spinner {
|
|
1608
|
+
display: inline-block;
|
|
1609
|
+
width: 10px;
|
|
1610
|
+
height: 10px;
|
|
1611
|
+
border: 2px solid currentColor;
|
|
1612
|
+
border-top-color: transparent;
|
|
1613
|
+
border-radius: 50%;
|
|
1614
|
+
animation: spin 0.8s linear infinite;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
.ca-task {
|
|
1618
|
+
font-size: 14px;
|
|
1619
|
+
line-height: 1.6;
|
|
1620
|
+
margin-bottom: 12px;
|
|
1621
|
+
white-space: pre-wrap;
|
|
1622
|
+
word-break: break-word;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
.ca-meta {
|
|
1626
|
+
display: flex;
|
|
1627
|
+
gap: 16px;
|
|
1628
|
+
font-size: 12px;
|
|
1629
|
+
font-family: var(--mono);
|
|
1630
|
+
color: var(--text-dim);
|
|
1631
|
+
margin-bottom: 12px;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
.ca-output {
|
|
1635
|
+
background: var(--surface-alt);
|
|
1636
|
+
border: 1px solid var(--border);
|
|
1637
|
+
border-radius: var(--radius-sm);
|
|
1638
|
+
padding: 12px 14px;
|
|
1639
|
+
font-family: var(--mono);
|
|
1640
|
+
font-size: 12px;
|
|
1641
|
+
line-height: 1.5;
|
|
1642
|
+
white-space: pre-wrap;
|
|
1643
|
+
word-break: break-word;
|
|
1644
|
+
max-height: 400px;
|
|
1645
|
+
overflow-y: auto;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
.ca-output-md {
|
|
1649
|
+
white-space: normal;
|
|
1650
|
+
font-family: var(--sans);
|
|
1651
|
+
max-height: 400px;
|
|
1652
|
+
overflow-y: auto;
|
|
1653
|
+
}
|
|
1654
|
+
.ca-output-md h1, .ca-output-md h2, .ca-output-md h3 { margin: 10px 0 4px; font-size: 14px; }
|
|
1655
|
+
.ca-output-md h2 { font-size: 15px; }
|
|
1656
|
+
.ca-output-md h1 { font-size: 16px; }
|
|
1657
|
+
.ca-output-md p { margin: 4px 0; }
|
|
1658
|
+
.ca-output-md ul, .ca-output-md ol { margin: 4px 0; padding-left: 20px; }
|
|
1659
|
+
.ca-output-md li { margin: 2px 0; }
|
|
1660
|
+
.ca-output-md code { background: var(--surface); padding: 1px 5px; border-radius: 3px; font-family: var(--mono); font-size: 0.9em; }
|
|
1661
|
+
.ca-output-md pre { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px; overflow-x: auto; font-size: 11px; }
|
|
1662
|
+
.ca-output-md pre code { background: none; padding: 0; }
|
|
1663
|
+
|
|
1664
|
+
.ca-error {
|
|
1665
|
+
background: var(--error-soft);
|
|
1666
|
+
border: 1px solid rgba(194, 84, 80, 0.15);
|
|
1667
|
+
color: var(--error);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
.ca-tree { margin-bottom: 16px; }
|
|
1671
|
+
.ca-tree-children {
|
|
1672
|
+
margin-left: 20px;
|
|
1673
|
+
padding-left: 16px;
|
|
1674
|
+
border-left: 2px solid var(--border);
|
|
1675
|
+
}
|
|
1676
|
+
.ca-tree-child {
|
|
1677
|
+
background: var(--surface);
|
|
1678
|
+
border: 1px solid var(--border-light);
|
|
1679
|
+
border-radius: var(--radius-sm);
|
|
1680
|
+
padding: 12px 16px;
|
|
1681
|
+
margin-bottom: 10px;
|
|
1682
|
+
font-size: 14px;
|
|
1683
|
+
cursor: pointer;
|
|
1684
|
+
}
|
|
1685
|
+
.ca-tree-child .ca-output {
|
|
1686
|
+
max-height: 300px;
|
|
1687
|
+
overflow-y: auto;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/* Digests */
|
|
1691
|
+
.digest-header {
|
|
1692
|
+
margin-bottom: 20px;
|
|
1693
|
+
padding-bottom: 12px;
|
|
1694
|
+
border-bottom: 1px solid var(--border);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
.digest-title {
|
|
1698
|
+
font-family: var(--serif);
|
|
1699
|
+
font-size: 20px;
|
|
1700
|
+
font-weight: 700;
|
|
1701
|
+
margin-bottom: 6px;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
.digest-meta {
|
|
1705
|
+
font-size: 13px;
|
|
1706
|
+
color: var(--text-dim);
|
|
1707
|
+
font-family: var(--mono);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
.digest-reader {
|
|
1711
|
+
font-size: 15px;
|
|
1712
|
+
line-height: 1.8;
|
|
1713
|
+
color: var(--text);
|
|
1714
|
+
padding: 0 4px;
|
|
1715
|
+
max-width: 720px;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
.digest-reader h3.digest-h3 {
|
|
1719
|
+
font-size: 18px; font-weight: 700;
|
|
1720
|
+
margin: 28px 0 12px 0; color: var(--accent);
|
|
1721
|
+
border-bottom: 1px solid var(--border); padding-bottom: 6px;
|
|
1722
|
+
}
|
|
1723
|
+
.digest-reader h4.digest-h4 { font-size: 16px; font-weight: 600; margin: 20px 0 8px 0; color: var(--text); }
|
|
1724
|
+
.digest-reader hr.digest-hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
|
|
1725
|
+
.digest-reader a.digest-link { color: var(--accent); word-break: break-all; font-size: 13px; font-family: var(--mono); }
|
|
1726
|
+
.digest-reader a.digest-link:hover { text-decoration: underline; }
|
|
1727
|
+
.digest-reader strong { color: var(--text); font-weight: 600; }
|
|
1728
|
+
|
|
1729
|
+
.digest-section-header {
|
|
1730
|
+
font-size: 16px; font-weight: 700; color: var(--accent);
|
|
1731
|
+
margin: 24px 0 12px 0; padding: 8px 12px;
|
|
1732
|
+
background: var(--surface-alt); border-radius: var(--radius-sm);
|
|
1733
|
+
border-left: 3px solid var(--accent);
|
|
1734
|
+
}
|
|
1735
|
+
.digest-section-header:first-child { margin-top: 0; }
|
|
1736
|
+
|
|
1737
|
+
.digest-item-title { font-size: 15px; font-weight: 600; color: var(--text); margin-top: 12px; line-height: 1.4; }
|
|
1738
|
+
.digest-stats { font-size: 13px; color: var(--text-dim); font-family: var(--mono); margin: 2px 0; }
|
|
1739
|
+
.digest-link-line { font-size: 13px; margin: 2px 0 8px 0; }
|
|
1740
|
+
.digest-link-line .digest-link-icon { margin-right: 2px; }
|
|
1741
|
+
.digest-link-line .digest-link { color: var(--accent); font-family: var(--mono); font-size: 12px; word-break: break-all; }
|
|
1742
|
+
.digest-link-line .digest-link:hover { text-decoration: underline; }
|
|
1743
|
+
.digest-line { font-size: 14px; color: var(--text-dim); line-height: 1.5; margin: 1px 0; }
|
|
1744
|
+
.digest-spacer { height: 4px; }
|
|
1745
|
+
.digest-hr { border: none; border-top: 1px solid var(--border); margin: 16px 0; }
|
|
1746
|
+
|
|
1747
|
+
.digest-articles { padding: 8px 0; }
|
|
1748
|
+
.digest-article-card {
|
|
1749
|
+
padding: 10px 14px;
|
|
1750
|
+
border-bottom: 1px solid var(--border-light);
|
|
1751
|
+
display: flex; flex-direction: column; gap: 4px;
|
|
1752
|
+
}
|
|
1753
|
+
.digest-article-card:last-child { border-bottom: none; }
|
|
1754
|
+
.digest-article-card .source-badge { font-size: 11px; color: var(--accent); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
1755
|
+
.digest-article-card .article-title { font-size: 15px; font-weight: 600; color: var(--text); text-decoration: none; line-height: 1.3; }
|
|
1756
|
+
.digest-article-card .article-title:hover { text-decoration: underline; color: var(--accent); }
|
|
1757
|
+
.digest-article-card .article-stats { font-size: 13px; color: var(--text-dim); font-family: var(--mono); }
|
|
1758
|
+
|
|
1759
|
+
.source-badge { font-size: 11px; color: var(--accent); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
1760
|
+
|
|
1761
|
+
/* Article list */
|
|
1762
|
+
.article-list { display: flex; flex-direction: column; gap: 10px; }
|
|
1763
|
+
.article-item {
|
|
1764
|
+
background: var(--surface); border: 1px solid var(--border-light);
|
|
1765
|
+
border-radius: var(--radius-sm); padding: 14px;
|
|
1766
|
+
transition: background 0.12s ease, border-color 0.12s ease;
|
|
1767
|
+
}
|
|
1768
|
+
.article-item:hover { background: var(--surface-alt); border-color: var(--accent); }
|
|
1769
|
+
.article-item.read { opacity: 0.7; }
|
|
1770
|
+
.article-item.read .article-title { text-decoration: line-through; color: var(--text-dim); }
|
|
1771
|
+
|
|
1772
|
+
.article-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; margin-bottom: 6px; }
|
|
1773
|
+
.article-title { font-weight: 600; font-size: 15px; line-height: 1.4; flex: 1; }
|
|
1774
|
+
.article-title a { color: var(--text); }
|
|
1775
|
+
.article-title a:hover { color: var(--accent); }
|
|
1776
|
+
.article-actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
1777
|
+
.article-meta { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; font-size: 12px; color: var(--text-dim); font-family: var(--mono); margin-bottom: 6px; }
|
|
1778
|
+
.article-source { color: var(--accent); font-weight: 500; }
|
|
1779
|
+
.article-score, .article-comments { background: var(--surface-alt); padding: 2px 8px; border-radius: 999px; }
|
|
1780
|
+
.article-summary { font-size: 13px; line-height: 1.5; color: var(--text-dim); }
|
|
1781
|
+
|
|
1782
|
+
.read-btn {
|
|
1783
|
+
padding: 4px 10px; font-size: 12px; border-radius: var(--radius-sm);
|
|
1784
|
+
background: var(--surface-alt); border: 1px solid var(--border);
|
|
1785
|
+
color: var(--text-dim); cursor: pointer; transition: all 0.12s ease;
|
|
1786
|
+
}
|
|
1787
|
+
.read-btn:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
1788
|
+
.read-btn.mark-unread { background: var(--success); color: #fff; border-color: var(--success); }
|
|
1789
|
+
|
|
1790
|
+
/* Status dot */
|
|
1791
|
+
.status-dot {
|
|
1792
|
+
width: 9px; height: 9px; border-radius: 50%;
|
|
1793
|
+
background: var(--success); display: inline-block;
|
|
1794
|
+
}
|
|
1795
|
+
.status-dot.error { background: var(--error); }
|
|
1796
|
+
|
|
1797
|
+
/* ══════════════════════════════════════════════════
|
|
1798
|
+
RESPONSIVE
|
|
1799
|
+
══════════════════════════════════════════════════ */
|
|
1800
|
+
@media (max-width: 1024px) {
|
|
1801
|
+
.shell { grid-template-columns: 1fr; }
|
|
1802
|
+
.sidebar {
|
|
1803
|
+
position: static; height: auto;
|
|
1804
|
+
flex-direction: row; overflow-x: auto;
|
|
1805
|
+
border-bottom: 1px solid var(--border-light);
|
|
1806
|
+
}
|
|
1807
|
+
.sidebar-section, .sidebar-footer { display: flex; gap: 4px; }
|
|
1808
|
+
.sidebar-spacer { display: none; }
|
|
1809
|
+
.main { padding: 20px 16px; }
|
|
1810
|
+
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
1811
|
+
.split, .split-view { grid-template-columns: 1fr; height: auto; }
|
|
1812
|
+
.split-list, .split-left { max-height: 50vh; }
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
@media (max-width: 768px) {
|
|
1816
|
+
.stats-grid { grid-template-columns: 1fr; }
|
|
1817
|
+
.grid { grid-template-columns: 1fr; }
|
|
1818
|
+
.cron-grid { grid-template-columns: 1fr; }
|
|
1819
|
+
}
|
|
1820
|
+
</style>
|
|
1821
|
+
</head>
|
|
1822
|
+
<body>
|
|
1823
|
+
|
|
1824
|
+
<div class="shell">
|
|
1825
|
+
<!-- ══════════════════════════════════════════════
|
|
1826
|
+
SIDEBAR
|
|
1827
|
+
══════════════════════════════════════════════ -->
|
|
1828
|
+
<nav class="sidebar">
|
|
1829
|
+
<div class="sidebar-header">
|
|
1830
|
+
<div class="sidebar-logo">\u{1F459}\u{1F99E}</div>
|
|
1831
|
+
<div class="sidebar-brand">
|
|
1832
|
+
<div class="sidebar-brand-name">SkimpyClaw</div>
|
|
1833
|
+
<div class="sidebar-brand-status">Online</div>
|
|
1834
|
+
</div>
|
|
1835
|
+
</div>
|
|
1836
|
+
|
|
1837
|
+
<div class="sidebar-section">
|
|
1838
|
+
<div class="sidebar-section-label">Dashboard</div>
|
|
1839
|
+
|
|
1840
|
+
<button class="sidebar-item active" data-page="overview" onclick="switchPage('overview')">
|
|
1841
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>
|
|
1842
|
+
Overview
|
|
1843
|
+
</button>
|
|
1844
|
+
|
|
1845
|
+
<button class="sidebar-item" data-page="history" onclick="switchPage('history')">
|
|
1846
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
1847
|
+
History
|
|
1848
|
+
</button>
|
|
1849
|
+
|
|
1850
|
+
<button class="sidebar-item" data-page="approvals" onclick="switchPage('approvals')">
|
|
1851
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
1852
|
+
Approvals
|
|
1853
|
+
</button>
|
|
1854
|
+
|
|
1855
|
+
<button class="sidebar-item" data-page="digests" onclick="switchPage('digests')">
|
|
1856
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
|
|
1857
|
+
Digests
|
|
1858
|
+
</button>
|
|
1859
|
+
|
|
1860
|
+
<button class="sidebar-item" data-page="audit" onclick="switchPage('audit')">
|
|
1861
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
|
1862
|
+
Audit
|
|
1863
|
+
</button>
|
|
1864
|
+
|
|
1865
|
+
<button class="sidebar-item" data-page="coding" onclick="switchPage('coding')">
|
|
1866
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
|
1867
|
+
Coding Agent
|
|
1868
|
+
</button>
|
|
1869
|
+
</div>
|
|
1870
|
+
|
|
1871
|
+
<div class="sidebar-section">
|
|
1872
|
+
<div class="sidebar-section-label">Settings</div>
|
|
1873
|
+
|
|
1874
|
+
<button class="sidebar-item" data-page="memory" onclick="switchPage('memory')">
|
|
1875
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
|
|
1876
|
+
Memory
|
|
1877
|
+
</button>
|
|
1878
|
+
|
|
1879
|
+
<button class="sidebar-item" data-page="templates" onclick="switchPage('templates')">
|
|
1880
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
|
1881
|
+
Templates
|
|
1882
|
+
</button>
|
|
1883
|
+
|
|
1884
|
+
<button class="sidebar-item" data-page="model" onclick="switchPage('model')">
|
|
1885
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
|
1886
|
+
Model
|
|
1887
|
+
</button>
|
|
1888
|
+
|
|
1889
|
+
<button class="sidebar-item" data-page="skills" onclick="switchPage('skills')">
|
|
1890
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
|
1891
|
+
Skills
|
|
1892
|
+
</button>
|
|
1893
|
+
|
|
1894
|
+
<button class="sidebar-item" data-page="cron" onclick="switchPage('cron')">
|
|
1895
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
1896
|
+
Cron Jobs
|
|
1897
|
+
</button>
|
|
1898
|
+
|
|
1899
|
+
<button class="sidebar-item" data-page="config" onclick="switchPage('config')">
|
|
1900
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
|
1901
|
+
Config
|
|
1902
|
+
</button>
|
|
1903
|
+
</div>
|
|
1904
|
+
|
|
1905
|
+
<div class="sidebar-spacer"></div>
|
|
1906
|
+
|
|
1907
|
+
<div class="sidebar-footer">
|
|
1908
|
+
<button class="sidebar-footer-item" data-page="logs" onclick="switchPage('logs')">
|
|
1909
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><line x1="4" y1="9" x2="20" y2="9"/><line x1="4" y1="15" x2="20" y2="15"/><line x1="10" y1="3" x2="8" y2="21"/><line x1="16" y1="3" x2="14" y2="21"/></svg>
|
|
1910
|
+
Logs
|
|
1911
|
+
</button>
|
|
1912
|
+
<button class="sidebar-footer-item" data-page="health" onclick="switchPage('health')">
|
|
1913
|
+
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
|
1914
|
+
Health
|
|
1915
|
+
</button>
|
|
1916
|
+
</div>
|
|
1917
|
+
</nav>
|
|
1918
|
+
|
|
1919
|
+
<!-- ══════════════════════════════════════════════
|
|
1920
|
+
MAIN CONTENT
|
|
1921
|
+
══════════════════════════════════════════════ -->
|
|
1922
|
+
<div class="main">
|
|
1923
|
+
|
|
1924
|
+
<div class="page-header">
|
|
1925
|
+
<div class="page-title" id="pageTitle">Overview</div>
|
|
1926
|
+
<div class="header-actions">
|
|
1927
|
+
<span class="header-meta" id="headerMeta"></span>
|
|
1928
|
+
<button class="theme-toggle" id="themeToggle" onclick="toggleTheme()" title="Toggle theme"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></button>
|
|
1929
|
+
</div>
|
|
1930
|
+
</div>
|
|
1931
|
+
|
|
1932
|
+
<!-- Overview Page -->
|
|
1933
|
+
<div class="page active" id="page-overview">
|
|
1934
|
+
<div id="welcomeBanner"></div>
|
|
1935
|
+
<div id="approvalBannerContainer"></div>
|
|
1936
|
+
<div class="stats-grid" id="statusGrid"></div>
|
|
1937
|
+
<div class="section">
|
|
1938
|
+
<div class="section-header">
|
|
1939
|
+
<div class="section-title">Recent Activity</div>
|
|
1940
|
+
<a class="section-link" onclick="switchPage('history')">View all \u2192</a>
|
|
1941
|
+
</div>
|
|
1942
|
+
<div id="overviewFeed"></div>
|
|
1943
|
+
</div>
|
|
1944
|
+
<div class="section">
|
|
1945
|
+
<div class="section-header">
|
|
1946
|
+
<div class="section-title">Scheduled Jobs</div>
|
|
1947
|
+
<a class="section-link" onclick="switchPage('cron')">Manage \u2192</a>
|
|
1948
|
+
</div>
|
|
1949
|
+
<div id="statusCronJobs"></div>
|
|
1950
|
+
</div>
|
|
1951
|
+
<div class="section">
|
|
1952
|
+
<div class="section-header">
|
|
1953
|
+
<div class="section-title">System Health</div>
|
|
1954
|
+
<a class="section-link" onclick="switchPage('health')">Details \u2192</a>
|
|
1955
|
+
</div>
|
|
1956
|
+
<div id="overviewHealthGrid"></div>
|
|
1957
|
+
</div>
|
|
1958
|
+
</div>
|
|
1959
|
+
|
|
1960
|
+
<!-- History Page -->
|
|
1961
|
+
<div class="page" id="page-history">
|
|
1962
|
+
<div class="toolbar" style="margin-bottom:16px;">
|
|
1963
|
+
<select id="historyTriggerFilter" style="width:160px;">
|
|
1964
|
+
<option value="">All triggers</option>
|
|
1965
|
+
<option value="telegram">Telegram</option>
|
|
1966
|
+
<option value="cron">Cron</option>
|
|
1967
|
+
<option value="discord">Discord</option>
|
|
1968
|
+
<option value="api">API</option>
|
|
1969
|
+
<option value="system">System</option>
|
|
1970
|
+
<option value="code_agent">Coding Agent</option>
|
|
1971
|
+
<option value="code_team">Coding Team</option>
|
|
1972
|
+
</select>
|
|
1973
|
+
<button class="btn btn-sm" id="historyRefreshBtn">Refresh</button>
|
|
1974
|
+
<span id="historyCount" style="font-size:13px;color:var(--text-muted);margin-left:auto;"></span>
|
|
1975
|
+
</div>
|
|
1976
|
+
<div class="split-view">
|
|
1977
|
+
<div class="split-left" id="historyList"></div>
|
|
1978
|
+
<div class="split-right" id="historyDetail">
|
|
1979
|
+
<div class="empty-state"><div class="empty-state-icon"><svg width="40" height="40" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></div><div class="empty-state-text">Select a trace to view details</div></div>
|
|
1980
|
+
</div>
|
|
1981
|
+
</div>
|
|
1982
|
+
</div>
|
|
1983
|
+
|
|
1984
|
+
<!-- Memory Page -->
|
|
1985
|
+
<div class="page" id="page-memory">
|
|
1986
|
+
<div class="toolbar">
|
|
1987
|
+
<label style="margin-bottom:0;">Agent:</label>
|
|
1988
|
+
<select id="memoryAgentSelect" style="width:200px;"></select>
|
|
1989
|
+
</div>
|
|
1990
|
+
<div class="split" style="height:calc(100vh - 200px);">
|
|
1991
|
+
<div class="split-list" id="memoryFileList"></div>
|
|
1992
|
+
<div class="split-detail" id="memoryContent">
|
|
1993
|
+
<div class="empty">Select a file to view</div>
|
|
1994
|
+
</div>
|
|
1995
|
+
</div>
|
|
1996
|
+
</div>
|
|
1997
|
+
|
|
1998
|
+
<!-- Cron Page -->
|
|
1999
|
+
<div class="page" id="page-cron">
|
|
2000
|
+
<div id="cronJobs"></div>
|
|
2001
|
+
</div>
|
|
2002
|
+
|
|
2003
|
+
<!-- Model Page -->
|
|
2004
|
+
<div class="page" id="page-model">
|
|
2005
|
+
<div class="card" style="margin-bottom:20px;">
|
|
2006
|
+
<div class="card-title">Current Model</div>
|
|
2007
|
+
<div class="card-value" id="currentModel">-</div>
|
|
2008
|
+
</div>
|
|
2009
|
+
<div class="card" style="margin-bottom:20px;">
|
|
2010
|
+
<div class="card-title">Switch Model</div>
|
|
2011
|
+
<div class="toolbar" style="margin-top:8px;">
|
|
2012
|
+
<input type="text" id="modelInput" placeholder="Model ID or alias" style="flex:1;width:auto;">
|
|
2013
|
+
<button class="btn" id="modelSwitchBtn">Switch</button>
|
|
2014
|
+
</div>
|
|
2015
|
+
</div>
|
|
2016
|
+
<div class="card" style="margin-bottom:20px;">
|
|
2017
|
+
<div class="card-title">Aliases</div>
|
|
2018
|
+
<div id="modelAliases"></div>
|
|
2019
|
+
</div>
|
|
2020
|
+
<div class="card">
|
|
2021
|
+
<div class="card-title">Agent Models</div>
|
|
2022
|
+
<div id="agentModels"></div>
|
|
2023
|
+
</div>
|
|
2024
|
+
</div>
|
|
2025
|
+
|
|
2026
|
+
<!-- Templates Page -->
|
|
2027
|
+
<div class="page" id="page-templates">
|
|
2028
|
+
<div class="toolbar">
|
|
2029
|
+
<label style="margin-bottom:0;">Agent:</label>
|
|
2030
|
+
<select id="templateAgentSelect" style="width:200px;"></select>
|
|
2031
|
+
</div>
|
|
2032
|
+
<div class="split" style="height:calc(100vh - 200px);">
|
|
2033
|
+
<div class="split-list" id="templateFileList"></div>
|
|
2034
|
+
<div class="split-detail" id="templateEditor">
|
|
2035
|
+
<div class="empty">Select a template to edit</div>
|
|
2036
|
+
</div>
|
|
2037
|
+
</div>
|
|
2038
|
+
</div>
|
|
2039
|
+
|
|
2040
|
+
<!-- Approvals Page -->
|
|
2041
|
+
<div class="page" id="page-approvals">
|
|
2042
|
+
<div class="toolbar">
|
|
2043
|
+
<button class="btn btn-small" id="approvalsRefreshBtn">Refresh</button>
|
|
2044
|
+
<label class="toggle-label"><input type="checkbox" id="approvalsAutoRefresh" checked> Auto-refresh</label>
|
|
2045
|
+
<span id="approvalsCount" style="font-size:13px;color:var(--text-dim);margin-left:auto;"></span>
|
|
2046
|
+
</div>
|
|
2047
|
+
<div id="approvalsPending"></div>
|
|
2048
|
+
<h3 class="section-title" style="margin-top:20px;">Recent</h3>
|
|
2049
|
+
<div id="approvalsRecent"></div>
|
|
2050
|
+
</div>
|
|
2051
|
+
|
|
2052
|
+
<!-- Coding Agent Page -->
|
|
2053
|
+
<div class="page" id="page-coding">
|
|
2054
|
+
<div id="caStatus">
|
|
2055
|
+
<div class="empty">No coding agents have run yet. Send a coding task via Telegram or Discord.</div>
|
|
2056
|
+
</div>
|
|
2057
|
+
</div>
|
|
2058
|
+
|
|
2059
|
+
<!-- Audit Page -->
|
|
2060
|
+
<div class="page" id="page-audit">
|
|
2061
|
+
<div class="toolbar">
|
|
2062
|
+
<label style="margin-bottom:0;">Trigger:</label>
|
|
2063
|
+
<select id="auditTriggerFilter" style="width:160px;">
|
|
2064
|
+
<option value="">All</option>
|
|
2065
|
+
<option value="telegram">Telegram</option>
|
|
2066
|
+
<option value="cron">Cron</option>
|
|
2067
|
+
<option value="api">API</option>
|
|
2068
|
+
<option value="system">System</option>
|
|
2069
|
+
<option value="discord">Discord</option>
|
|
2070
|
+
<option value="code_agent">Coding Agent</option>
|
|
2071
|
+
</select>
|
|
2072
|
+
<button class="btn btn-small" id="auditRefreshBtn">Refresh</button>
|
|
2073
|
+
<span id="auditCount" style="font-size:13px;color:var(--text-dim);margin-left:auto;"></span>
|
|
2074
|
+
</div>
|
|
2075
|
+
<div id="auditEntries"></div>
|
|
2076
|
+
<div style="text-align:center;margin-top:12px;">
|
|
2077
|
+
<button class="btn btn-small" id="auditLoadMoreBtn" style="display:none;">Load More</button>
|
|
2078
|
+
</div>
|
|
2079
|
+
</div>
|
|
2080
|
+
|
|
2081
|
+
<!-- Digests Page -->
|
|
2082
|
+
<div class="page" id="page-digests">
|
|
2083
|
+
<div class="split">
|
|
2084
|
+
<div class="split-list" id="digestList"></div>
|
|
2085
|
+
<div class="split-detail" id="digestDetail">
|
|
2086
|
+
<div class="empty">Select a digest to view articles</div>
|
|
2087
|
+
</div>
|
|
2088
|
+
</div>
|
|
2089
|
+
</div>
|
|
2090
|
+
|
|
2091
|
+
<!-- Skills Page -->
|
|
2092
|
+
<div class="page" id="page-skills">
|
|
2093
|
+
<div class="toolbar">
|
|
2094
|
+
<button class="btn btn-small" id="skillsRefreshBtn">Refresh</button>
|
|
2095
|
+
<button class="btn btn-small btn-success" id="skillsCreateBtn">Create Skill</button>
|
|
2096
|
+
<span id="skillsCount" style="font-size:13px;color:var(--text-dim);margin-left:auto;"></span>
|
|
2097
|
+
</div>
|
|
2098
|
+
<div id="skillsList"></div>
|
|
2099
|
+
<div id="skillsCreateForm" style="display:none;margin-top:16px;">
|
|
2100
|
+
<div class="card">
|
|
2101
|
+
<div class="card-title">Create New Skill</div>
|
|
2102
|
+
<div style="margin-bottom:8px;">
|
|
2103
|
+
<label>Name (alphanumeric + hyphens):</label>
|
|
2104
|
+
<input type="text" id="skillNameInput" placeholder="my-skill" style="width:300px;">
|
|
2105
|
+
</div>
|
|
2106
|
+
<label>SKILL.md content:</label>
|
|
2107
|
+
<textarea id="skillContentInput" style="min-height:300px;" placeholder="---\\nname: my-skill\\ndescription: What this skill does\\nemoji: \\"🔧\\"\\ntags: [\\"example\\"]\\n---\\n\\n# My Skill\\n\\nSkill documentation here..."></textarea>
|
|
2108
|
+
<div class="toolbar" style="margin-top:8px;">
|
|
2109
|
+
<button class="btn btn-success" id="skillSaveNewBtn">Save</button>
|
|
2110
|
+
<button class="btn btn-small" id="skillCancelCreateBtn">Cancel</button>
|
|
2111
|
+
</div>
|
|
2112
|
+
</div>
|
|
2113
|
+
</div>
|
|
2114
|
+
<div id="skillDetail" style="display:none;margin-top:16px;"></div>
|
|
2115
|
+
</div>
|
|
2116
|
+
|
|
2117
|
+
<!-- Logs Page -->
|
|
2118
|
+
<div class="page" id="page-logs">
|
|
2119
|
+
<div class="split">
|
|
2120
|
+
<div class="split-list" id="logFileList"></div>
|
|
2121
|
+
<div class="split-detail" id="logViewer" style="padding:0;">
|
|
2122
|
+
<div class="toolbar" style="padding:12px;border-bottom:1px solid var(--border);">
|
|
2123
|
+
<label class="toggle-label"><input type="checkbox" id="logTailMode"> Tail mode</label>
|
|
2124
|
+
<input type="number" id="logTailLines" value="100" min="10" max="10000" style="width:80px;" placeholder="Lines">
|
|
2125
|
+
<label class="toggle-label"><input type="checkbox" id="logAutoRefresh"> Auto-refresh</label>
|
|
2126
|
+
<button class="btn btn-small" id="logRefreshBtn">Refresh</button>
|
|
2127
|
+
</div>
|
|
2128
|
+
<div class="log-viewer" id="logContent">Select a log file to view</div>
|
|
2129
|
+
</div>
|
|
2130
|
+
</div>
|
|
2131
|
+
</div>
|
|
2132
|
+
|
|
2133
|
+
<!-- Config Page -->
|
|
2134
|
+
<div class="page" id="page-config">
|
|
2135
|
+
<div class="card">
|
|
2136
|
+
<div class="card-title">Configuration (secrets redacted)</div>
|
|
2137
|
+
<textarea id="configEditor" style="min-height:500px;"></textarea>
|
|
2138
|
+
<div class="toolbar" style="margin-top:12px;">
|
|
2139
|
+
<button class="btn" id="configValidateBtn">Validate</button>
|
|
2140
|
+
<button class="btn btn-success" id="configSaveBtn">Save</button>
|
|
2141
|
+
<span id="configStatus" style="font-size:12px;"></span>
|
|
2142
|
+
</div>
|
|
2143
|
+
<div style="margin-top:8px;font-size:12px;color:var(--warning);">
|
|
2144
|
+
Warning: Restart required for changes to take effect.
|
|
2145
|
+
</div>
|
|
2146
|
+
</div>
|
|
2147
|
+
</div>
|
|
2148
|
+
|
|
2149
|
+
<!-- Health Page -->
|
|
2150
|
+
<div class="page" id="page-health">
|
|
2151
|
+
<div class="card" id="doctorSummaryCard">
|
|
2152
|
+
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center;">
|
|
2153
|
+
System Health
|
|
2154
|
+
<div style="display:flex;gap:8px;align-items:center;">
|
|
2155
|
+
<span id="doctorTimestamp" style="font-size:11px;color:var(--text-dim);font-weight:normal;"></span>
|
|
2156
|
+
<button class="btn" id="healthRecheckBtn" style="font-size:12px;">Re-check</button>
|
|
2157
|
+
</div>
|
|
2158
|
+
</div>
|
|
2159
|
+
<div id="doctorSummary" style="margin-bottom:8px;">Loading...</div>
|
|
2160
|
+
</div>
|
|
2161
|
+
<div id="doctorCategories">Loading...</div>
|
|
2162
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px;">
|
|
2163
|
+
<div class="card">
|
|
2164
|
+
<div class="card-title">Environment Variables</div>
|
|
2165
|
+
<div id="healthEnvVars">Loading...</div>
|
|
2166
|
+
</div>
|
|
2167
|
+
<div class="card">
|
|
2168
|
+
<div class="card-title">Feature Toggles</div>
|
|
2169
|
+
<div id="healthFeatures">Loading...</div>
|
|
2170
|
+
</div>
|
|
2171
|
+
</div>
|
|
2172
|
+
</div>
|
|
2173
|
+
</div>
|
|
2174
|
+
</div>
|
|
2175
|
+
|
|
2176
|
+
<script>
|
|
2177
|
+
// --- Theme ---
|
|
2178
|
+
function initTheme() {
|
|
2179
|
+
const saved = localStorage.getItem('skimpyclaw-theme') || 'light';
|
|
2180
|
+
document.documentElement.setAttribute('data-theme', saved);
|
|
2181
|
+
updateThemeIcon(saved);
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
function toggleTheme() {
|
|
2185
|
+
const current = document.documentElement.getAttribute('data-theme') || 'light';
|
|
2186
|
+
const next = current === 'dark' ? 'light' : 'dark';
|
|
2187
|
+
document.documentElement.setAttribute('data-theme', next);
|
|
2188
|
+
localStorage.setItem('skimpyclaw-theme', next);
|
|
2189
|
+
updateThemeIcon(next);
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
function updateThemeIcon(theme) {
|
|
2193
|
+
var btn = document.getElementById('themeToggle');
|
|
2194
|
+
if (!btn) return;
|
|
2195
|
+
if (theme === 'dark') {
|
|
2196
|
+
btn.innerHTML = '<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
|
|
2197
|
+
} else {
|
|
2198
|
+
btn.innerHTML = '<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
initTheme();
|
|
2203
|
+
// --- State ---
|
|
2204
|
+
let statusInterval = null;
|
|
2205
|
+
let logAutoRefreshInterval = null;
|
|
2206
|
+
let currentLogFile = null;
|
|
2207
|
+
let currentSessionId = null;
|
|
2208
|
+
let currentTemplateFile = null;
|
|
2209
|
+
let templateUnsaved = false;
|
|
2210
|
+
let agentList = [];
|
|
2211
|
+
|
|
2212
|
+
// --- Utilities ---
|
|
2213
|
+
function esc(str) {
|
|
2214
|
+
const d = document.createElement('div');
|
|
2215
|
+
d.textContent = str;
|
|
2216
|
+
return d.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
function md(str) {
|
|
2220
|
+
if (typeof marked !== 'undefined' && marked.parse) {
|
|
2221
|
+
try { return marked.parse(str, { breaks: true }); } catch(e) {}
|
|
2222
|
+
}
|
|
2223
|
+
return esc(str);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
function isSafeHref(url) {
|
|
2227
|
+
if (!url) return false;
|
|
2228
|
+
if (url.startsWith('#') || url.startsWith('/')) return true;
|
|
2229
|
+
try {
|
|
2230
|
+
const parsed = new URL(url, window.location.origin);
|
|
2231
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:' || parsed.protocol === 'mailto:';
|
|
2232
|
+
} catch {
|
|
2233
|
+
return false;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
function renderInlineMarkdown(text) {
|
|
2238
|
+
let html = esc(text || '');
|
|
2239
|
+
|
|
2240
|
+
html = html.replace(/\\x60([^\\x60]+)\\x60/g, '<code>$1</code>');
|
|
2241
|
+
html = html.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
|
|
2242
|
+
html = html.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');
|
|
2243
|
+
|
|
2244
|
+
html = html.replace(/\\[([^\\]]+)\\]\\(([^)\\s]+)\\)/g, (m, label, href) => {
|
|
2245
|
+
return isSafeHref(href)
|
|
2246
|
+
? '<a href="' + esc(href) + '" target="_blank" rel="noopener noreferrer">' + label + '</a>'
|
|
2247
|
+
: label;
|
|
2248
|
+
});
|
|
2249
|
+
|
|
2250
|
+
html = html.replace(/(^|\\s)(https?:\\/\\/[^\\s<]+)/g, (m, prefix, url) => {
|
|
2251
|
+
return isSafeHref(url)
|
|
2252
|
+
? prefix + '<a href="' + esc(url) + '" target="_blank" rel="noopener noreferrer">' + esc(url) + '</a>'
|
|
2253
|
+
: m;
|
|
2254
|
+
});
|
|
2255
|
+
|
|
2256
|
+
return html;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
function renderMarkdown(md) {
|
|
2260
|
+
const src = String(md || '').replace(/\\r\\n/g, '\\n');
|
|
2261
|
+
if (!src.trim()) return '<div class="empty">(empty)</div>';
|
|
2262
|
+
|
|
2263
|
+
const lines = src.split('\\n');
|
|
2264
|
+
const out = [];
|
|
2265
|
+
let i = 0;
|
|
2266
|
+
|
|
2267
|
+
while (i < lines.length) {
|
|
2268
|
+
const line = lines[i];
|
|
2269
|
+
|
|
2270
|
+
if (/^\\x60\\x60\\x60/.test(line.trim())) {
|
|
2271
|
+
const code = [];
|
|
2272
|
+
i++;
|
|
2273
|
+
while (i < lines.length && !/^\\x60\\x60\\x60/.test(lines[i].trim())) {
|
|
2274
|
+
code.push(lines[i]);
|
|
2275
|
+
i++;
|
|
2276
|
+
}
|
|
2277
|
+
if (i < lines.length) i++;
|
|
2278
|
+
out.push('<pre><code>' + esc(code.join('\\n')) + '</code></pre>');
|
|
2279
|
+
continue;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
const heading = line.match(/^(#{1,6})\\s+(.*)$/);
|
|
2283
|
+
if (heading) {
|
|
2284
|
+
const level = heading[1].length;
|
|
2285
|
+
out.push('<h' + level + '>' + renderInlineMarkdown(heading[2]) + '</h' + level + '>');
|
|
2286
|
+
i++;
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
if (/^\\s*([-*_])\\1\\1+\\s*$/.test(line)) {
|
|
2291
|
+
out.push('<hr>');
|
|
2292
|
+
i++;
|
|
2293
|
+
continue;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
const quote = line.match(/^>\\s?(.*)$/);
|
|
2297
|
+
if (quote) {
|
|
2298
|
+
out.push('<blockquote>' + renderInlineMarkdown(quote[1]) + '</blockquote>');
|
|
2299
|
+
i++;
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
const ul = line.match(/^\\s*[-*]\\s+(.*)$/);
|
|
2304
|
+
if (ul) {
|
|
2305
|
+
const items = [];
|
|
2306
|
+
while (i < lines.length) {
|
|
2307
|
+
const m = lines[i].match(/^\\s*[-*]\\s+(.*)$/);
|
|
2308
|
+
if (!m) break;
|
|
2309
|
+
items.push('<li>' + renderInlineMarkdown(m[1]) + '</li>');
|
|
2310
|
+
i++;
|
|
2311
|
+
}
|
|
2312
|
+
out.push('<ul>' + items.join('') + '</ul>');
|
|
2313
|
+
continue;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
const ol = line.match(/^\\s*\\d+\\.\\s+(.*)$/);
|
|
2317
|
+
if (ol) {
|
|
2318
|
+
const items = [];
|
|
2319
|
+
while (i < lines.length) {
|
|
2320
|
+
const m = lines[i].match(/^\\s*\\d+\\.\\s+(.*)$/);
|
|
2321
|
+
if (!m) break;
|
|
2322
|
+
items.push('<li>' + renderInlineMarkdown(m[1]) + '</li>');
|
|
2323
|
+
i++;
|
|
2324
|
+
}
|
|
2325
|
+
out.push('<ol>' + items.join('') + '</ol>');
|
|
2326
|
+
continue;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
if (!line.trim()) {
|
|
2330
|
+
i++;
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
const paragraph = [line.trim()];
|
|
2335
|
+
i++;
|
|
2336
|
+
while (i < lines.length && lines[i].trim() &&
|
|
2337
|
+
!/^(#{1,6})\\s+/.test(lines[i]) &&
|
|
2338
|
+
!/^\\s*([-*_])\\1\\1+\\s*$/.test(lines[i]) &&
|
|
2339
|
+
!/^>\\s?/.test(lines[i]) &&
|
|
2340
|
+
!/^\\s*[-*]\\s+/.test(lines[i]) &&
|
|
2341
|
+
!/^\\s*\\d+\\.\\s+/.test(lines[i]) &&
|
|
2342
|
+
!/^\\x60\\x60\\x60/.test(lines[i].trim())) {
|
|
2343
|
+
paragraph.push(lines[i].trim());
|
|
2344
|
+
i++;
|
|
2345
|
+
}
|
|
2346
|
+
out.push('<p>' + renderInlineMarkdown(paragraph.join(' ')) + '</p>');
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
return '<div class="markdown-content">' + out.join('') + '</div>';
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
function formatUptime(seconds) {
|
|
2353
|
+
const d = Math.floor(seconds / 86400);
|
|
2354
|
+
const h = Math.floor((seconds % 86400) / 3600);
|
|
2355
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
2356
|
+
const s = Math.floor(seconds % 60);
|
|
2357
|
+
const parts = [];
|
|
2358
|
+
if (d > 0) parts.push(d + 'd');
|
|
2359
|
+
if (h > 0) parts.push(h + 'h');
|
|
2360
|
+
if (m > 0) parts.push(m + 'm');
|
|
2361
|
+
parts.push(s + 's');
|
|
2362
|
+
return parts.join(' ');
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
function formatDate(dateStr) {
|
|
2366
|
+
if (!dateStr) return 'N/A';
|
|
2367
|
+
const d = new Date(dateStr);
|
|
2368
|
+
return d.toLocaleString();
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
function formatSize(bytes) {
|
|
2372
|
+
if (bytes < 1024) return bytes + ' B';
|
|
2373
|
+
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
2374
|
+
return (bytes / 1048576).toFixed(1) + ' MB';
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
function getToken() {
|
|
2378
|
+
return localStorage.getItem('skimpyclaw_token') || '';
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
function setToken(token) {
|
|
2382
|
+
localStorage.setItem('skimpyclaw_token', token);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
function showTokenPrompt() {
|
|
2386
|
+
const existing = document.getElementById('tokenOverlay');
|
|
2387
|
+
if (existing) return; // Already showing
|
|
2388
|
+
|
|
2389
|
+
const overlay = document.createElement('div');
|
|
2390
|
+
overlay.id = 'tokenOverlay';
|
|
2391
|
+
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;z-index:9999;';
|
|
2392
|
+
overlay.innerHTML =
|
|
2393
|
+
'<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:24px;width:400px;max-width:90vw;">' +
|
|
2394
|
+
'<h3 style="margin-bottom:12px;font-size:14px;">Dashboard Token Required</h3>' +
|
|
2395
|
+
'<p style="font-size:12px;color:var(--text-dim);margin-bottom:12px;">Enter the token shown in the server console output.</p>' +
|
|
2396
|
+
'<input type="text" id="tokenInput" placeholder="Paste token here" style="margin-bottom:12px;">' +
|
|
2397
|
+
'<button class="btn btn-success" id="tokenSubmitBtn" style="width:100%;">Connect</button>' +
|
|
2398
|
+
'</div>';
|
|
2399
|
+
document.body.appendChild(overlay);
|
|
2400
|
+
|
|
2401
|
+
document.getElementById('tokenSubmitBtn').addEventListener('click', () => {
|
|
2402
|
+
const token = document.getElementById('tokenInput').value.trim();
|
|
2403
|
+
if (token) {
|
|
2404
|
+
setToken(token);
|
|
2405
|
+
overlay.remove();
|
|
2406
|
+
// Retry loading
|
|
2407
|
+
startStatusRefresh();
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
|
|
2411
|
+
document.getElementById('tokenInput').addEventListener('keydown', (e) => {
|
|
2412
|
+
if (e.key === 'Enter') {
|
|
2413
|
+
document.getElementById('tokenSubmitBtn').click();
|
|
2414
|
+
}
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
async function api(path, options) {
|
|
2419
|
+
try {
|
|
2420
|
+
const opts = options || {};
|
|
2421
|
+
opts.headers = opts.headers || {};
|
|
2422
|
+
const token = getToken();
|
|
2423
|
+
if (token) {
|
|
2424
|
+
opts.headers['Authorization'] = 'Bearer ' + token;
|
|
2425
|
+
}
|
|
2426
|
+
const res = await fetch('/api/dashboard/' + path, opts);
|
|
2427
|
+
if (res.status === 401) {
|
|
2428
|
+
showTokenPrompt();
|
|
2429
|
+
throw new Error('Unauthorized');
|
|
2430
|
+
}
|
|
2431
|
+
if (!res.ok) {
|
|
2432
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
2433
|
+
throw new Error(err.error || res.statusText);
|
|
2434
|
+
}
|
|
2435
|
+
return await res.json();
|
|
2436
|
+
} catch (e) {
|
|
2437
|
+
console.error('API error:', path, e);
|
|
2438
|
+
throw e;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
function showToast(message, type) {
|
|
2443
|
+
const t = document.createElement('div');
|
|
2444
|
+
t.className = 'toast ' + (type || 'success');
|
|
2445
|
+
t.textContent = message;
|
|
2446
|
+
document.body.appendChild(t);
|
|
2447
|
+
setTimeout(() => t.remove(), 3000);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
|
|
2451
|
+
// --- Page Navigation ---
|
|
2452
|
+
function switchPage(name) {
|
|
2453
|
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
2454
|
+
document.querySelectorAll('.sidebar-item, .sidebar-footer-item').forEach(b => b.classList.remove('active'));
|
|
2455
|
+
var page = document.getElementById('page-' + name);
|
|
2456
|
+
var btn = document.querySelector('[data-page="' + name + '"]');
|
|
2457
|
+
if (page) page.classList.add('active');
|
|
2458
|
+
if (btn) btn.classList.add('active');
|
|
2459
|
+
var titles = { overview:'Overview', history:'History', approvals:'Approvals', digests:'Digests', coding:'Coding Agent', audit:'Audit', memory:'Memory', model:'Model', templates:'Templates', skills:'Skills', cron:'Cron Jobs', config:'Config', logs:'Logs', health:'Health' };
|
|
2460
|
+
document.getElementById('pageTitle').textContent = titles[name] || name;
|
|
2461
|
+
onPageActivated(name);
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
function onPageActivated(page) {
|
|
2465
|
+
// Clear status refresh when leaving overview
|
|
2466
|
+
if (page !== 'overview' && statusInterval) {
|
|
2467
|
+
clearInterval(statusInterval);
|
|
2468
|
+
statusInterval = null;
|
|
2469
|
+
}
|
|
2470
|
+
if (page === 'overview') startStatusRefresh();
|
|
2471
|
+
else if (page === 'history') loadHistory();
|
|
2472
|
+
else if (page === 'memory') loadMemory();
|
|
2473
|
+
else if (page === 'approvals') startApprovalsPolling();
|
|
2474
|
+
else if (page === 'cron') loadCronJobs();
|
|
2475
|
+
else if (page === 'model') loadModel();
|
|
2476
|
+
else if (page === 'templates') loadTemplates();
|
|
2477
|
+
else if (page === 'coding') startCaPolling();
|
|
2478
|
+
else if (page === 'audit') loadAudit();
|
|
2479
|
+
else if (page === 'logs') loadLogFiles();
|
|
2480
|
+
else if (page === 'digests') loadDigests();
|
|
2481
|
+
else if (page === 'skills') loadSkills();
|
|
2482
|
+
else if (page === 'config') loadConfig();
|
|
2483
|
+
else if (page === 'health') loadHealth();
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
// --- Status Tab ---
|
|
2487
|
+
function getGreeting() {
|
|
2488
|
+
var h = new Date().getHours();
|
|
2489
|
+
if (h < 12) return 'Good morning';
|
|
2490
|
+
if (h < 17) return 'Good afternoon';
|
|
2491
|
+
return 'Good evening';
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
function formatStartDate(uptimeSeconds) {
|
|
2495
|
+
var started = new Date(Date.now() - uptimeSeconds * 1000);
|
|
2496
|
+
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
2497
|
+
return months[started.getMonth()] + ' ' + started.getDate() + ' ' +
|
|
2498
|
+
(started.getHours() < 10 ? '0' : '') + started.getHours() + ':' +
|
|
2499
|
+
(started.getMinutes() < 10 ? '0' : '') + started.getMinutes();
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
function formatNextCronRun(dateStr) {
|
|
2503
|
+
if (!dateStr) return '';
|
|
2504
|
+
var d = new Date(dateStr);
|
|
2505
|
+
var now = new Date();
|
|
2506
|
+
var diffMs = d.getTime() - now.getTime();
|
|
2507
|
+
if (diffMs < 0) return 'overdue';
|
|
2508
|
+
var diffH = Math.floor(diffMs / 3600000);
|
|
2509
|
+
var diffM = Math.floor((diffMs % 3600000) / 60000);
|
|
2510
|
+
if (diffH > 24) return Math.floor(diffH / 24) + 'd';
|
|
2511
|
+
if (diffH > 0) return diffH + 'h ' + diffM + 'm';
|
|
2512
|
+
return diffM + 'm';
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
async function loadStatus() {
|
|
2516
|
+
try {
|
|
2517
|
+
var data = await api('status');
|
|
2518
|
+
// Update header meta
|
|
2519
|
+
var metaEl = document.getElementById('headerMeta');
|
|
2520
|
+
if (metaEl) {
|
|
2521
|
+
metaEl.textContent = esc(data.model || '') + ' \\u00B7 up ' + formatUptime(data.uptime);
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// Welcome banner
|
|
2525
|
+
var welcomeEl = document.getElementById('welcomeBanner');
|
|
2526
|
+
var cronCount = data.cronJobs ? data.cronJobs.length : 0;
|
|
2527
|
+
var subagentsDone = data.subagents ? data.subagents.recentCompleted : 0;
|
|
2528
|
+
welcomeEl.innerHTML =
|
|
2529
|
+
'<div class="welcome-banner">' +
|
|
2530
|
+
'<div class="welcome-greeting">' + getGreeting() + ', Katrina</div>' +
|
|
2531
|
+
'<div class="welcome-sub">' +
|
|
2532
|
+
'Up ' + formatUptime(data.uptime) +
|
|
2533
|
+
' \\u00B7 ' + cronCount + ' cron job' + (cronCount !== 1 ? 's' : '') +
|
|
2534
|
+
' \\u00B7 ' + subagentsDone + ' task' + (subagentsDone !== 1 ? 's' : '') + ' completed' +
|
|
2535
|
+
' \\u00B7 ' + esc(data.model || 'no model') +
|
|
2536
|
+
'</div>' +
|
|
2537
|
+
'</div>';
|
|
2538
|
+
|
|
2539
|
+
// Approval banner (fetch pending approvals)
|
|
2540
|
+
var bannerEl = document.getElementById('approvalBannerContainer');
|
|
2541
|
+
try {
|
|
2542
|
+
var appData = await api('approvals');
|
|
2543
|
+
var pending = appData.pending || [];
|
|
2544
|
+
if (pending.length > 0) {
|
|
2545
|
+
var a = pending[0];
|
|
2546
|
+
var serverNow = appData.now ? new Date(appData.now).getTime() : Date.now();
|
|
2547
|
+
var exp = new Date(a.expiresAt).getTime();
|
|
2548
|
+
var remaining = Math.max(0, Math.round((exp - serverNow) / 1000));
|
|
2549
|
+
var m = Math.floor(remaining / 60);
|
|
2550
|
+
var s = remaining % 60;
|
|
2551
|
+
var countdown = m + ':' + (s < 10 ? '0' : '') + s;
|
|
2552
|
+
bannerEl.innerHTML =
|
|
2553
|
+
'<div class="approval-banner">' +
|
|
2554
|
+
'<div class="approval-banner-icon"><svg width="20" height="20" fill="none" stroke="var(--warning)" stroke-width="2" viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>' +
|
|
2555
|
+
'<div class="approval-banner-content">' +
|
|
2556
|
+
'<div class="approval-banner-title">Pending Approval' + (pending.length > 1 ? ' (' + pending.length + ')' : '') + '</div>' +
|
|
2557
|
+
'<div class="approval-banner-detail">' + esc(a.command) + ' \\u2014 Tier ' + a.tier + ' \\u00B7 expires in ' + countdown + '</div>' +
|
|
2558
|
+
'</div>' +
|
|
2559
|
+
'<div class="approval-banner-actions">' +
|
|
2560
|
+
'<button class="btn btn-success btn-sm" onclick="approveApproval(\\'' + esc(a.id) + '\\')">Approve</button>' +
|
|
2561
|
+
'<button class="btn btn-danger btn-sm" onclick="denyApproval(\\'' + esc(a.id) + '\\')">Deny</button>' +
|
|
2562
|
+
'</div>' +
|
|
2563
|
+
'</div>';
|
|
2564
|
+
} else {
|
|
2565
|
+
bannerEl.innerHTML = '';
|
|
2566
|
+
}
|
|
2567
|
+
} catch(e2) {
|
|
2568
|
+
bannerEl.innerHTML = '';
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
// Stat cards
|
|
2572
|
+
var grid = document.getElementById('statusGrid');
|
|
2573
|
+
var nextCron = '';
|
|
2574
|
+
if (data.cronJobs && data.cronJobs.length > 0) {
|
|
2575
|
+
var soonest = data.cronJobs.reduce(function(a, b) {
|
|
2576
|
+
return new Date(a.nextRun) < new Date(b.nextRun) ? a : b;
|
|
2577
|
+
});
|
|
2578
|
+
nextCron = 'Next: ' + esc(soonest.name || soonest.id) + ' in ' + formatNextCronRun(soonest.nextRun);
|
|
2579
|
+
}
|
|
2580
|
+
grid.innerHTML =
|
|
2581
|
+
'<div class="stat-card">' +
|
|
2582
|
+
'<div class="stat-icon sage"><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>' +
|
|
2583
|
+
'<div class="stat-label">Uptime</div>' +
|
|
2584
|
+
'<div class="stat-value">' + formatUptime(data.uptime) + '</div>' +
|
|
2585
|
+
'<div class="stat-sub">Started ' + formatStartDate(data.uptime) + '</div>' +
|
|
2586
|
+
'</div>' +
|
|
2587
|
+
'<div class="stat-card">' +
|
|
2588
|
+
'<div class="stat-icon green"><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></div>' +
|
|
2589
|
+
'<div class="stat-label">Model</div>' +
|
|
2590
|
+
'<div class="stat-value" style="font-size:18px;">' + esc(data.model || '-') + '</div>' +
|
|
2591
|
+
'<div class="stat-sub">Agent: ' + esc(data.agent || '-') + '</div>' +
|
|
2592
|
+
'</div>' +
|
|
2593
|
+
'<div class="stat-card">' +
|
|
2594
|
+
'<div class="stat-icon amber"><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>' +
|
|
2595
|
+
'<div class="stat-label">Cron Jobs</div>' +
|
|
2596
|
+
'<div class="stat-value">' + cronCount + '</div>' +
|
|
2597
|
+
'<div class="stat-sub">' + (nextCron || 'No upcoming jobs') + '</div>' +
|
|
2598
|
+
'</div>' +
|
|
2599
|
+
'<div class="stat-card">' +
|
|
2600
|
+
'<div class="stat-icon blue"><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg></div>' +
|
|
2601
|
+
'<div class="stat-label">Subagents</div>' +
|
|
2602
|
+
'<div class="stat-value">' + (data.subagents ? data.subagents.active : 0) + ' active</div>' +
|
|
2603
|
+
'<div class="stat-sub">' + subagentsDone + ' completed \\u00B7 ' + (data.subagents ? data.subagents.recentFailed : 0) + ' failed</div>' +
|
|
2604
|
+
'</div>';
|
|
2605
|
+
|
|
2606
|
+
// Cron jobs grid
|
|
2607
|
+
var cronEl = document.getElementById('statusCronJobs');
|
|
2608
|
+
if (data.cronJobs && data.cronJobs.length > 0) {
|
|
2609
|
+
cronEl.innerHTML = '<div class="cron-grid">' + data.cronJobs.map(function(j) {
|
|
2610
|
+
return '<div class="cron-card">' +
|
|
2611
|
+
'<div><div class="cron-name">' + esc(j.name || j.id) + '</div>' +
|
|
2612
|
+
'<div class="cron-schedule">' + esc(j.schedule ? j.schedule.expr || '' : '') + '</div></div>' +
|
|
2613
|
+
'<div class="cron-next">Next: ' + formatNextCronRun(j.nextRun) + '</div>' +
|
|
2614
|
+
'</div>';
|
|
2615
|
+
}).join('') + '</div>';
|
|
2616
|
+
} else {
|
|
2617
|
+
cronEl.innerHTML = '<div class="empty">No cron jobs configured</div>';
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
// Recent activity feed (from audit traces)
|
|
2621
|
+
var feedEl = document.getElementById('overviewFeed');
|
|
2622
|
+
if (feedEl) {
|
|
2623
|
+
try {
|
|
2624
|
+
var auditData = await api('audit?limit=8&offset=0');
|
|
2625
|
+
var traces = auditData.traces || [];
|
|
2626
|
+
if (traces.length > 0) {
|
|
2627
|
+
feedEl.innerHTML = '<div class="feed-card"><div class="feed">' + traces.map(function(t) {
|
|
2628
|
+
var trigger = t.trigger || 'system';
|
|
2629
|
+
var iconClass = TRIGGER_ICON_CLASS[trigger] || 'system';
|
|
2630
|
+
var icon = TRIGGER_ICONS[trigger] || TRIGGER_ICONS.system;
|
|
2631
|
+
var summary = getTraceSummary(t);
|
|
2632
|
+
var evtCount = t.events ? t.events.length : 0;
|
|
2633
|
+
var dur = formatDuration(t.durationMs);
|
|
2634
|
+
var statusDot = t.status === 'ok' ? '' : ' <span style="color:var(--error);">\\u25CF</span>';
|
|
2635
|
+
return '<div class="feed-item">' +
|
|
2636
|
+
'<div class="feed-icon ' + iconClass + '">' + icon + '</div>' +
|
|
2637
|
+
'<div><div class="feed-title">' + esc(trigger) + statusDot + '</div>' +
|
|
2638
|
+
'<div class="feed-detail">' + esc(summary.slice(0, 100)) + '</div></div>' +
|
|
2639
|
+
'<div class="feed-time">' + formatTimeAgo(t.startedAt) + '<br><span style="font-size:10px;">' + evtCount + ' events \\u00B7 ' + dur + '</span></div>' +
|
|
2640
|
+
'</div>';
|
|
2641
|
+
}).join('') + '</div></div>';
|
|
2642
|
+
} else {
|
|
2643
|
+
feedEl.innerHTML = '<div class="empty">No recent activity</div>';
|
|
2644
|
+
}
|
|
2645
|
+
} catch(e3) {
|
|
2646
|
+
feedEl.innerHTML = '<div class="empty">Could not load activity</div>';
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// Health summary
|
|
2651
|
+
var healthEl = document.getElementById('overviewHealthGrid');
|
|
2652
|
+
if (healthEl) {
|
|
2653
|
+
try {
|
|
2654
|
+
var docData = await api('doctor');
|
|
2655
|
+
var checks = docData.report ? docData.report.checks : [];
|
|
2656
|
+
var topChecks = checks.slice(0, 5);
|
|
2657
|
+
if (topChecks.length > 0) {
|
|
2658
|
+
healthEl.innerHTML = '<div class="health-card"><div class="health-grid">' + topChecks.map(function(ch) {
|
|
2659
|
+
var dotClass = ch.ok ? 'pass' : (ch.fatal ? 'fail' : 'warn');
|
|
2660
|
+
return '<div class="health-row">' +
|
|
2661
|
+
'<div class="health-dot ' + dotClass + '"></div>' +
|
|
2662
|
+
'<div class="health-name">' + esc(ch.name) + '</div>' +
|
|
2663
|
+
'<div class="health-value">' + esc(ch.detail) + '</div>' +
|
|
2664
|
+
'</div>';
|
|
2665
|
+
}).join('') + '</div></div>';
|
|
2666
|
+
} else {
|
|
2667
|
+
healthEl.innerHTML = '<div class="empty">No health checks</div>';
|
|
2668
|
+
}
|
|
2669
|
+
} catch(e4) {
|
|
2670
|
+
healthEl.innerHTML = '';
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
} catch (e) {
|
|
2675
|
+
var metaEl2 = document.getElementById('headerMeta');
|
|
2676
|
+
if (metaEl2) metaEl2.textContent = 'Offline';
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
function startStatusRefresh() {
|
|
2681
|
+
loadStatus();
|
|
2682
|
+
statusInterval = setInterval(loadStatus, 5000);
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
// --- History Tab (Audit Traces) ---
|
|
2686
|
+
var historyTraces = [];
|
|
2687
|
+
var historyOffset = 0;
|
|
2688
|
+
var HISTORY_PAGE_SIZE = 50;
|
|
2689
|
+
|
|
2690
|
+
var TRIGGER_ICONS = {
|
|
2691
|
+
telegram: '<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
|
|
2692
|
+
cron: '<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
|
|
2693
|
+
discord: '<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
|
|
2694
|
+
code_agent: '<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
|
|
2695
|
+
code_team: '<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
|
|
2696
|
+
api: '<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>',
|
|
2697
|
+
system: '<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9"/></svg>'
|
|
2698
|
+
};
|
|
2699
|
+
|
|
2700
|
+
var TRIGGER_ICON_CLASS = {
|
|
2701
|
+
telegram: 'telegram', cron: 'cron', discord: 'telegram',
|
|
2702
|
+
code_agent: 'code', code_team: 'code', api: 'system', system: 'system'
|
|
2703
|
+
};
|
|
2704
|
+
|
|
2705
|
+
function getTraceSummary(trace) {
|
|
2706
|
+
if (!trace.events || trace.events.length === 0) return 'No events';
|
|
2707
|
+
// Find first non-tool event, or first event summary
|
|
2708
|
+
for (var i = 0; i < trace.events.length; i++) {
|
|
2709
|
+
var ev = trace.events[i];
|
|
2710
|
+
if (ev.type === 'response' || ev.type === 'message') return ev.summary || '';
|
|
2711
|
+
}
|
|
2712
|
+
// Fallback: count tool uses
|
|
2713
|
+
var toolUses = trace.events.filter(function(e) { return e.type === 'tool_use'; }).length;
|
|
2714
|
+
var firstTool = trace.events[0];
|
|
2715
|
+
if (toolUses > 0) return toolUses + ' tool call' + (toolUses !== 1 ? 's' : '') + (firstTool ? ' \\u2014 ' + firstTool.summary.split('(')[0] : '');
|
|
2716
|
+
return trace.events[0].summary || '';
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
function formatTimeAgo(dateStr) {
|
|
2720
|
+
if (!dateStr) return '';
|
|
2721
|
+
var now = Date.now();
|
|
2722
|
+
var then = new Date(dateStr).getTime();
|
|
2723
|
+
var diffMs = now - then;
|
|
2724
|
+
var diffMin = Math.floor(diffMs / 60000);
|
|
2725
|
+
if (diffMin < 1) return 'just now';
|
|
2726
|
+
if (diffMin < 60) return diffMin + 'm ago';
|
|
2727
|
+
var diffH = Math.floor(diffMin / 60);
|
|
2728
|
+
if (diffH < 24) return diffH + 'h ago';
|
|
2729
|
+
var diffD = Math.floor(diffH / 24);
|
|
2730
|
+
if (diffD === 1) return 'yesterday';
|
|
2731
|
+
if (diffD < 7) return diffD + 'd ago';
|
|
2732
|
+
return formatDate(dateStr);
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
async function loadHistory(append) {
|
|
2736
|
+
if (!append) {
|
|
2737
|
+
historyOffset = 0;
|
|
2738
|
+
historyTraces = [];
|
|
2739
|
+
}
|
|
2740
|
+
var triggerFilter = document.getElementById('historyTriggerFilter').value;
|
|
2741
|
+
var path = 'audit?limit=' + HISTORY_PAGE_SIZE + '&offset=' + historyOffset;
|
|
2742
|
+
if (triggerFilter) path += '&trigger=' + encodeURIComponent(triggerFilter);
|
|
2743
|
+
|
|
2744
|
+
try {
|
|
2745
|
+
var data = await api(path);
|
|
2746
|
+
var list = document.getElementById('historyList');
|
|
2747
|
+
var countEl = document.getElementById('historyCount');
|
|
2748
|
+
var traces = data.traces || [];
|
|
2749
|
+
historyTraces = historyTraces.concat(traces);
|
|
2750
|
+
|
|
2751
|
+
if (historyTraces.length === 0) {
|
|
2752
|
+
list.innerHTML = '<div style="padding:24px;" class="empty-state"><div class="empty-state-text">No activity found</div></div>';
|
|
2753
|
+
countEl.textContent = '0 traces';
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
countEl.textContent = (data.total || historyTraces.length) + ' total';
|
|
2758
|
+
|
|
2759
|
+
var html = historyTraces.map(function(t) {
|
|
2760
|
+
var trigger = t.trigger || 'system';
|
|
2761
|
+
var iconClass = TRIGGER_ICON_CLASS[trigger] || 'system';
|
|
2762
|
+
var icon = TRIGGER_ICONS[trigger] || TRIGGER_ICONS.system;
|
|
2763
|
+
var summary = getTraceSummary(t);
|
|
2764
|
+
var evtCount = t.events ? t.events.length : 0;
|
|
2765
|
+
var statusDot = t.status === 'ok' ? '<span style="color:var(--success);">\\u25CF</span>' : '<span style="color:var(--error);">\\u25CF</span>';
|
|
2766
|
+
var dur = formatDuration(t.durationMs);
|
|
2767
|
+
|
|
2768
|
+
return '<div class="list-item" data-trace-id="' + esc(t.traceId) + '">' +
|
|
2769
|
+
'<div style="display:flex;align-items:center;gap:10px;">' +
|
|
2770
|
+
'<div class="feed-icon ' + iconClass + '" style="width:32px;height:32px;font-size:14px;flex-shrink:0;">' + icon + '</div>' +
|
|
2771
|
+
'<div style="flex:1;min-width:0;">' +
|
|
2772
|
+
'<div class="list-item-title" style="display:flex;align-items:center;gap:6px;">' +
|
|
2773
|
+
statusDot + ' ' + esc(trigger) +
|
|
2774
|
+
'<span style="font-weight:400;color:var(--text-muted);font-size:12px;">' + evtCount + ' events \\u00B7 ' + dur + '</span>' +
|
|
2775
|
+
'</div>' +
|
|
2776
|
+
'<div class="list-item-meta" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + esc(summary.slice(0, 80)) + '</div>' +
|
|
2777
|
+
'</div>' +
|
|
2778
|
+
'<div style="font-size:11px;color:var(--text-muted);font-family:var(--mono);white-space:nowrap;">' + formatTimeAgo(t.startedAt) + '</div>' +
|
|
2779
|
+
'</div>' +
|
|
2780
|
+
'</div>';
|
|
2781
|
+
}).join('');
|
|
2782
|
+
|
|
2783
|
+
// Add load more if there are more
|
|
2784
|
+
if (data.total && historyOffset + traces.length < data.total) {
|
|
2785
|
+
html += '<div class="list-item" style="text-align:center;color:var(--accent);font-weight:500;cursor:pointer;" id="historyLoadMore">Load more...</div>';
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
list.innerHTML = html;
|
|
2789
|
+
|
|
2790
|
+
// Attach click handlers
|
|
2791
|
+
list.querySelectorAll('.list-item[data-trace-id]').forEach(function(item) {
|
|
2792
|
+
item.addEventListener('click', function() { loadHistoryTrace(item.getAttribute('data-trace-id')); });
|
|
2793
|
+
});
|
|
2794
|
+
var loadMoreBtn = document.getElementById('historyLoadMore');
|
|
2795
|
+
if (loadMoreBtn) {
|
|
2796
|
+
loadMoreBtn.addEventListener('click', function() {
|
|
2797
|
+
historyOffset += HISTORY_PAGE_SIZE;
|
|
2798
|
+
loadHistory(true);
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
} catch (e) {
|
|
2802
|
+
document.getElementById('historyList').innerHTML = '<div style="padding:24px;" class="empty-state"><div class="empty-state-text">Failed to load history</div></div>';
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
function loadHistoryTrace(traceId) {
|
|
2807
|
+
// Highlight in list
|
|
2808
|
+
document.querySelectorAll('#historyList .list-item').forEach(function(i) {
|
|
2809
|
+
i.classList.toggle('active', i.getAttribute('data-trace-id') === traceId);
|
|
2810
|
+
});
|
|
2811
|
+
|
|
2812
|
+
var trace = historyTraces.find(function(t) { return t.traceId === traceId; });
|
|
2813
|
+
var detail = document.getElementById('historyDetail');
|
|
2814
|
+
if (!trace) {
|
|
2815
|
+
detail.innerHTML = '<div class="empty-state"><div class="empty-state-text">Trace not found</div></div>';
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
var trigger = trace.trigger || 'system';
|
|
2820
|
+
var statusColor = trace.status === 'ok' ? 'var(--success)' : 'var(--error)';
|
|
2821
|
+
|
|
2822
|
+
var html = '';
|
|
2823
|
+
// Header
|
|
2824
|
+
html += '<div style="margin-bottom:20px;">';
|
|
2825
|
+
html += '<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">';
|
|
2826
|
+
html += '<span style="font-family:var(--serif);font-size:18px;font-weight:700;">' + esc(trigger) + '</span>';
|
|
2827
|
+
html += '<span class="badge-status ' + (trace.status === 'ok' ? 'completed' : 'failed') + '">' + esc(trace.status.toUpperCase()) + '</span>';
|
|
2828
|
+
html += '</div>';
|
|
2829
|
+
html += '<div style="font-size:12px;font-family:var(--mono);color:var(--text-muted);display:flex;gap:12px;flex-wrap:wrap;">';
|
|
2830
|
+
html += '<span>' + esc(trace.traceId) + '</span>';
|
|
2831
|
+
html += '<span>' + formatDuration(trace.durationMs) + '</span>';
|
|
2832
|
+
html += '<span>' + formatDate(trace.startedAt) + '</span>';
|
|
2833
|
+
if (trace.endedAt) html += '<span>\\u2192 ' + formatDate(trace.endedAt) + '</span>';
|
|
2834
|
+
html += '</div></div>';
|
|
2835
|
+
|
|
2836
|
+
// Events timeline
|
|
2837
|
+
if (trace.events && trace.events.length > 0) {
|
|
2838
|
+
html += '<div style="font-family:var(--serif);font-size:16px;font-weight:700;margin-bottom:12px;">Events (' + trace.events.length + ')</div>';
|
|
2839
|
+
html += '<div style="display:flex;flex-direction:column;gap:2px;">';
|
|
2840
|
+
for (var i = 0; i < trace.events.length; i++) {
|
|
2841
|
+
var ev = trace.events[i];
|
|
2842
|
+
var evColor = ev.type === 'tool_use' ? 'var(--accent)' : ev.type === 'response' ? 'var(--success)' : 'var(--text-dim)';
|
|
2843
|
+
var evDur = ev.durationMs != null ? formatDuration(ev.durationMs) : '';
|
|
2844
|
+
html += '<div style="display:grid;grid-template-columns:80px 1fr auto;gap:8px;padding:8px 0;border-bottom:1px solid var(--border-light);align-items:start;">';
|
|
2845
|
+
html += '<span style="font-size:11px;font-weight:600;color:' + evColor + ';font-family:var(--mono);text-transform:uppercase;">' + esc(ev.type || '-') + '</span>';
|
|
2846
|
+
html += '<span style="font-size:13px;line-height:1.45;word-break:break-word;">' + esc(ev.summary || '') + '</span>';
|
|
2847
|
+
html += '<span style="font-size:11px;color:var(--text-muted);font-family:var(--mono);white-space:nowrap;">' + evDur + '</span>';
|
|
2848
|
+
html += '</div>';
|
|
2849
|
+
}
|
|
2850
|
+
html += '</div>';
|
|
2851
|
+
} else {
|
|
2852
|
+
html += '<div class="empty-state"><div class="empty-state-text">No events in this trace</div></div>';
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
detail.innerHTML = html;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
document.getElementById('historyRefreshBtn').addEventListener('click', function() { loadHistory(false); });
|
|
2859
|
+
document.getElementById('historyTriggerFilter').addEventListener('change', function() { loadHistory(false); });
|
|
2860
|
+
|
|
2861
|
+
// --- Memory Tab ---
|
|
2862
|
+
async function loadMemory() {
|
|
2863
|
+
await populateAgentSelects();
|
|
2864
|
+
const agentId = document.getElementById('memoryAgentSelect').value;
|
|
2865
|
+
if (!agentId) return;
|
|
2866
|
+
try {
|
|
2867
|
+
const data = await api('memory/' + encodeURIComponent(agentId));
|
|
2868
|
+
const list = document.getElementById('memoryFileList');
|
|
2869
|
+
let items = '<div class="list-item" data-file="curated"><div class="list-item-title">MEMORY.md</div><div class="list-item-sub">Curated memory</div></div>';
|
|
2870
|
+
if (data.files && data.files.length > 0) {
|
|
2871
|
+
items += data.files.map(f =>
|
|
2872
|
+
'<div class="list-item" data-file="' + esc(f.name) + '">' +
|
|
2873
|
+
'<div class="list-item-title">' + esc(f.name) + '</div>' +
|
|
2874
|
+
'<div class="list-item-sub">' + (f.size ? formatSize(f.size) : '') + '</div>' +
|
|
2875
|
+
'</div>'
|
|
2876
|
+
).join('');
|
|
2877
|
+
}
|
|
2878
|
+
list.innerHTML = items;
|
|
2879
|
+
list.querySelectorAll('.list-item').forEach(item => {
|
|
2880
|
+
item.addEventListener('click', () => loadMemoryFile(agentId, item.dataset.file));
|
|
2881
|
+
});
|
|
2882
|
+
} catch (e) {
|
|
2883
|
+
document.getElementById('memoryFileList').innerHTML = '<div class="empty">Failed to load memory files</div>';
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
async function loadMemoryFile(agentId, filename) {
|
|
2888
|
+
document.querySelectorAll('#memoryFileList .list-item').forEach(i => {
|
|
2889
|
+
i.classList.toggle('active', i.dataset.file === filename);
|
|
2890
|
+
});
|
|
2891
|
+
const el = document.getElementById('memoryContent');
|
|
2892
|
+
try {
|
|
2893
|
+
const data = await api('memory/' + encodeURIComponent(agentId) + '/' + encodeURIComponent(filename));
|
|
2894
|
+
const content = data.content || '';
|
|
2895
|
+
const isMarkdown = filename === 'curated' || String(filename).toLowerCase().endsWith('.md');
|
|
2896
|
+
el.innerHTML = isMarkdown
|
|
2897
|
+
? renderMarkdown(content)
|
|
2898
|
+
: '<pre style="white-space:pre-wrap;font-size:13px;line-height:1.6;">' + esc(content || '(empty)') + '</pre>';
|
|
2899
|
+
} catch (e) {
|
|
2900
|
+
el.innerHTML = '<div class="empty">Failed to load file</div>';
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
// --- Cron Tab ---
|
|
2905
|
+
async function loadCronJobs() {
|
|
2906
|
+
try {
|
|
2907
|
+
const data = await api('cron');
|
|
2908
|
+
const el = document.getElementById('cronJobs');
|
|
2909
|
+
if (!data.jobs || data.jobs.length === 0) {
|
|
2910
|
+
el.innerHTML = '<div class="empty">No cron jobs configured</div>';
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
el.innerHTML = data.jobs.map(j =>
|
|
2914
|
+
'<div class="card"><div class="cron-card">' +
|
|
2915
|
+
'<div class="cron-info">' +
|
|
2916
|
+
'<div class="cron-name">' + esc(j.name || j.id) + '</div>' +
|
|
2917
|
+
'<div class="cron-schedule">Schedule: ' + esc(j.schedule?.expr || '-') + (j.schedule?.tz ? ' (' + esc(j.schedule.tz) + ')' : '') + '</div>' +
|
|
2918
|
+
'<div class="cron-next">Next run: ' + formatDate(j.nextRun) + '</div>' +
|
|
2919
|
+
'</div>' +
|
|
2920
|
+
'<button class="btn btn-small" onclick="triggerCronJob(\\'' + esc(j.id) + '\\')">Run Now</button>' +
|
|
2921
|
+
'</div></div>'
|
|
2922
|
+
).join('');
|
|
2923
|
+
} catch (e) {
|
|
2924
|
+
document.getElementById('cronJobs').innerHTML = '<div class="empty">Failed to load cron jobs</div>';
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
async function triggerCronJob(id) {
|
|
2929
|
+
try {
|
|
2930
|
+
await api('cron/' + encodeURIComponent(id) + '/run', { method: 'POST' });
|
|
2931
|
+
showToast('Job triggered: ' + id);
|
|
2932
|
+
} catch (e) {
|
|
2933
|
+
showToast('Failed to trigger job: ' + e.message, 'error');
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
// --- Model Tab ---
|
|
2938
|
+
async function loadModel() {
|
|
2939
|
+
try {
|
|
2940
|
+
const data = await api('model');
|
|
2941
|
+
document.getElementById('currentModel').textContent = data.current || '-';
|
|
2942
|
+
|
|
2943
|
+
const aliasEl = document.getElementById('modelAliases');
|
|
2944
|
+
if (data.aliases && Object.keys(data.aliases).length > 0) {
|
|
2945
|
+
aliasEl.innerHTML = '<table style="width:100%;font-size:13px;">' +
|
|
2946
|
+
Object.entries(data.aliases).map(([k, v]) =>
|
|
2947
|
+
'<tr><td style="padding:4px 8px;color:var(--highlight);">' + esc(k) + '</td><td style="padding:4px 8px;">' + esc(String(v)) + '</td></tr>'
|
|
2948
|
+
).join('') + '</table>';
|
|
2949
|
+
} else {
|
|
2950
|
+
aliasEl.innerHTML = '<div style="color:var(--text-dim);font-size:13px;padding:8px;">No aliases configured</div>';
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
const agentEl = document.getElementById('agentModels');
|
|
2954
|
+
if (data.agents && Object.keys(data.agents).length > 0) {
|
|
2955
|
+
agentEl.innerHTML = '<table style="width:100%;font-size:13px;">' +
|
|
2956
|
+
Object.entries(data.agents).map(([k, v]) =>
|
|
2957
|
+
'<tr><td style="padding:4px 8px;color:var(--success);">' + esc(k) + '</td><td style="padding:4px 8px;">' + esc(String(v || 'default')) + '</td></tr>'
|
|
2958
|
+
).join('') + '</table>';
|
|
2959
|
+
} else {
|
|
2960
|
+
agentEl.innerHTML = '<div style="color:var(--text-dim);font-size:13px;padding:8px;">No agents configured</div>';
|
|
2961
|
+
}
|
|
2962
|
+
} catch (e) {
|
|
2963
|
+
document.getElementById('currentModel').textContent = 'Error loading';
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
document.getElementById('modelSwitchBtn').addEventListener('click', async () => {
|
|
2968
|
+
const model = document.getElementById('modelInput').value.trim();
|
|
2969
|
+
if (!model) return;
|
|
2970
|
+
try {
|
|
2971
|
+
await api('model', {
|
|
2972
|
+
method: 'POST',
|
|
2973
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2974
|
+
body: JSON.stringify({ model })
|
|
2975
|
+
});
|
|
2976
|
+
showToast('Model switched to ' + model);
|
|
2977
|
+
document.getElementById('modelInput').value = '';
|
|
2978
|
+
loadModel();
|
|
2979
|
+
} catch (e) {
|
|
2980
|
+
showToast('Failed to switch model: ' + e.message, 'error');
|
|
2981
|
+
}
|
|
2982
|
+
});
|
|
2983
|
+
|
|
2984
|
+
// --- Templates Tab ---
|
|
2985
|
+
async function loadTemplates() {
|
|
2986
|
+
await populateAgentSelects();
|
|
2987
|
+
const agentId = document.getElementById('templateAgentSelect').value;
|
|
2988
|
+
if (!agentId) return;
|
|
2989
|
+
try {
|
|
2990
|
+
const data = await api('templates/' + encodeURIComponent(agentId));
|
|
2991
|
+
const list = document.getElementById('templateFileList');
|
|
2992
|
+
if (!data.templates || data.templates.length === 0) {
|
|
2993
|
+
list.innerHTML = '<div class="empty">No templates found</div>';
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
list.innerHTML = data.templates.map(t =>
|
|
2997
|
+
'<div class="list-item" data-name="' + esc(t.name) + '">' +
|
|
2998
|
+
'<div class="list-item-title">' + esc(t.name) + '</div>' +
|
|
2999
|
+
'<div class="list-item-sub">' + (t.exists ? formatSize(t.size) : 'Not created') + '</div>' +
|
|
3000
|
+
'</div>'
|
|
3001
|
+
).join('');
|
|
3002
|
+
list.querySelectorAll('.list-item').forEach(item => {
|
|
3003
|
+
item.addEventListener('click', () => loadTemplate(agentId, item.dataset.name));
|
|
3004
|
+
});
|
|
3005
|
+
} catch (e) {
|
|
3006
|
+
document.getElementById('templateFileList').innerHTML = '<div class="empty">Failed to load templates</div>';
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
async function loadTemplate(agentId, name) {
|
|
3011
|
+
currentTemplateFile = name;
|
|
3012
|
+
templateUnsaved = false;
|
|
3013
|
+
document.querySelectorAll('#templateFileList .list-item').forEach(i => {
|
|
3014
|
+
i.classList.toggle('active', i.dataset.name === name);
|
|
3015
|
+
});
|
|
3016
|
+
const el = document.getElementById('templateEditor');
|
|
3017
|
+
try {
|
|
3018
|
+
const data = await api('templates/' + encodeURIComponent(agentId) + '/' + encodeURIComponent(name));
|
|
3019
|
+
el.innerHTML =
|
|
3020
|
+
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">' +
|
|
3021
|
+
'<div><strong>' + esc(name) + '</strong><span class="unsaved" id="unsavedIndicator" style="display:none;">unsaved changes</span></div>' +
|
|
3022
|
+
'<button class="btn btn-success btn-small" id="templateSaveBtn">Save</button>' +
|
|
3023
|
+
'</div>' +
|
|
3024
|
+
'<textarea id="templateTextarea" style="min-height:calc(100vh - 300px);">' + esc(data.content || '') + '</textarea>';
|
|
3025
|
+
document.getElementById('templateTextarea').addEventListener('input', () => {
|
|
3026
|
+
templateUnsaved = true;
|
|
3027
|
+
document.getElementById('unsavedIndicator').style.display = 'inline';
|
|
3028
|
+
});
|
|
3029
|
+
document.getElementById('templateSaveBtn').addEventListener('click', () => saveTemplate(agentId, name));
|
|
3030
|
+
} catch (e) {
|
|
3031
|
+
el.innerHTML = '<div class="empty">Failed to load template (may not exist yet)</div>';
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
async function saveTemplate(agentId, name) {
|
|
3036
|
+
const content = document.getElementById('templateTextarea').value;
|
|
3037
|
+
try {
|
|
3038
|
+
await api('templates/' + encodeURIComponent(agentId) + '/' + encodeURIComponent(name), {
|
|
3039
|
+
method: 'PUT',
|
|
3040
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3041
|
+
body: JSON.stringify({ content })
|
|
3042
|
+
});
|
|
3043
|
+
templateUnsaved = false;
|
|
3044
|
+
document.getElementById('unsavedIndicator').style.display = 'none';
|
|
3045
|
+
showToast('Template saved');
|
|
3046
|
+
} catch (e) {
|
|
3047
|
+
showToast('Failed to save: ' + e.message, 'error');
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
// --- Logs Tab ---
|
|
3052
|
+
async function loadLogFiles() {
|
|
3053
|
+
try {
|
|
3054
|
+
const data = await api('logs');
|
|
3055
|
+
const list = document.getElementById('logFileList');
|
|
3056
|
+
if (!data.files || data.files.length === 0) {
|
|
3057
|
+
list.innerHTML = '<div class="empty">No log files</div>';
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
list.innerHTML = data.files.map(f =>
|
|
3061
|
+
'<div class="list-item" data-name="' + esc(f.name) + '">' +
|
|
3062
|
+
'<div class="list-item-title">' + esc(f.name) + '</div>' +
|
|
3063
|
+
'<div class="list-item-sub">' + formatSize(f.size) + ' · ' + formatDate(f.modified) + '</div>' +
|
|
3064
|
+
'</div>'
|
|
3065
|
+
).join('');
|
|
3066
|
+
list.querySelectorAll('.list-item').forEach(item => {
|
|
3067
|
+
item.addEventListener('click', () => {
|
|
3068
|
+
currentLogFile = item.dataset.name;
|
|
3069
|
+
document.querySelectorAll('#logFileList .list-item').forEach(i => {
|
|
3070
|
+
i.classList.toggle('active', i.dataset.name === currentLogFile);
|
|
3071
|
+
});
|
|
3072
|
+
loadLogContent();
|
|
3073
|
+
});
|
|
3074
|
+
});
|
|
3075
|
+
} catch (e) {
|
|
3076
|
+
document.getElementById('logFileList').innerHTML = '<div class="empty">Failed to load logs</div>';
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
async function loadLogContent() {
|
|
3081
|
+
if (!currentLogFile) return;
|
|
3082
|
+
const tailMode = document.getElementById('logTailMode').checked;
|
|
3083
|
+
const tailLines = document.getElementById('logTailLines').value;
|
|
3084
|
+
let path = 'logs/' + encodeURIComponent(currentLogFile);
|
|
3085
|
+
if (tailMode && tailLines) {
|
|
3086
|
+
path += '?tail=' + encodeURIComponent(tailLines);
|
|
3087
|
+
}
|
|
3088
|
+
try {
|
|
3089
|
+
const data = await api(path);
|
|
3090
|
+
document.getElementById('logContent').textContent = data.content || '(empty)';
|
|
3091
|
+
// Auto-scroll to bottom
|
|
3092
|
+
const viewer = document.getElementById('logContent');
|
|
3093
|
+
viewer.scrollTop = viewer.scrollHeight;
|
|
3094
|
+
} catch (e) {
|
|
3095
|
+
document.getElementById('logContent').textContent = 'Failed to load log';
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
document.getElementById('logRefreshBtn').addEventListener('click', loadLogContent);
|
|
3100
|
+
document.getElementById('logTailMode').addEventListener('change', loadLogContent);
|
|
3101
|
+
document.getElementById('logAutoRefresh').addEventListener('change', (e) => {
|
|
3102
|
+
if (e.target.checked) {
|
|
3103
|
+
logAutoRefreshInterval = setInterval(loadLogContent, 3000);
|
|
3104
|
+
} else {
|
|
3105
|
+
clearInterval(logAutoRefreshInterval);
|
|
3106
|
+
logAutoRefreshInterval = null;
|
|
3107
|
+
}
|
|
3108
|
+
});
|
|
3109
|
+
|
|
3110
|
+
// --- Coding Agent Tab (Multi-Agent) ---
|
|
3111
|
+
let caPollingInterval = null;
|
|
3112
|
+
|
|
3113
|
+
function stopCaPolling() {
|
|
3114
|
+
if (caPollingInterval) {
|
|
3115
|
+
clearInterval(caPollingInterval);
|
|
3116
|
+
caPollingInterval = null;
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
function startCaPolling() {
|
|
3121
|
+
loadCaAgents();
|
|
3122
|
+
stopCaPolling();
|
|
3123
|
+
caPollingInterval = setInterval(loadCaAgents, 3000);
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
async function loadCaAgents() {
|
|
3127
|
+
try {
|
|
3128
|
+
const data = await api('code-agents');
|
|
3129
|
+
renderCaAgents(data.agents || []);
|
|
3130
|
+
} catch {
|
|
3131
|
+
document.getElementById('caStatus').innerHTML =
|
|
3132
|
+
'<div class="empty">Failed to load coding agents</div>';
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
function renderCaAgents(agents) {
|
|
3137
|
+
var el = document.getElementById('caStatus');
|
|
3138
|
+
if (!agents || agents.length === 0) {
|
|
3139
|
+
el.innerHTML = '<div class="empty">No coding agents have run yet.</div>';
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
// Save expanded states and scroll positions before re-rendering
|
|
3144
|
+
var expandedIds = new Set();
|
|
3145
|
+
var scrollPositions = {};
|
|
3146
|
+
var expandedTeammates = new Set();
|
|
3147
|
+
document.querySelectorAll('.audit-events.expanded').forEach(function(detailEl) {
|
|
3148
|
+
if (detailEl.id) {
|
|
3149
|
+
expandedIds.add(detailEl.id);
|
|
3150
|
+
var outputEl = detailEl.querySelector('.ca-output');
|
|
3151
|
+
if (outputEl) {
|
|
3152
|
+
scrollPositions[detailEl.id] = outputEl.scrollTop;
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
});
|
|
3156
|
+
// Save expanded teammate panels
|
|
3157
|
+
document.querySelectorAll('[id^="tm-"]').forEach(function(tmEl) {
|
|
3158
|
+
if (tmEl.style.display !== 'none') {
|
|
3159
|
+
expandedTeammates.add(tmEl.id);
|
|
3160
|
+
}
|
|
3161
|
+
});
|
|
3162
|
+
|
|
3163
|
+
// Build lookup for child tasks (parentTaskId → children)
|
|
3164
|
+
var childMap = {};
|
|
3165
|
+
for (var ci = 0; ci < agents.length; ci++) {
|
|
3166
|
+
if (agents[ci].parentTaskId) {
|
|
3167
|
+
if (!childMap[agents[ci].parentTaskId]) childMap[agents[ci].parentTaskId] = [];
|
|
3168
|
+
childMap[agents[ci].parentTaskId].push(agents[ci]);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
var html = '';
|
|
3173
|
+
for (var i = 0; i < agents.length; i++) {
|
|
3174
|
+
var a = agents[i];
|
|
3175
|
+
// Skip child tasks — they render under their parent
|
|
3176
|
+
if (a.parentTaskId) continue;
|
|
3177
|
+
var isActive = a.status === 'running' || a.status === 'validating';
|
|
3178
|
+
var spinner = isActive ? '<span class="ca-spinner"></span> ' : '';
|
|
3179
|
+
var secs = a.durationSeconds != null ? a.durationSeconds
|
|
3180
|
+
: Math.round((Date.now() - new Date(a.startedAt).getTime()) / 1000);
|
|
3181
|
+
var elapsed = secs < 60 ? secs + 's' : Math.floor(secs / 60) + 'm ' + (secs % 60) + 's';
|
|
3182
|
+
var taskPreview = (a.task || '').length > 80 ? a.task.slice(0, 80) + '...' : (a.task || '');
|
|
3183
|
+
var detailId = 'ca-detail-' + (a.id || '').replace(/[^a-zA-Z0-9]/g, '');
|
|
3184
|
+
var children = childMap[a.id] || [];
|
|
3185
|
+
var isTeam = a.agent === 'team-coordinator' && children.length > 0;
|
|
3186
|
+
|
|
3187
|
+
// Wrap team-coordinator agents in a tree container
|
|
3188
|
+
if (isTeam) html += '<div class="ca-tree">';
|
|
3189
|
+
|
|
3190
|
+
html += '<div class="audit-entry">';
|
|
3191
|
+
html += '<div class="audit-header">';
|
|
3192
|
+
html += '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">';
|
|
3193
|
+
html += '<span class="audit-id">' + esc(a.id) + '</span>';
|
|
3194
|
+
html += '<span class="ca-status-badge ' + esc(a.status) + '">' + spinner + esc(a.status.toUpperCase()) + '</span>';
|
|
3195
|
+
if (a.agent === 'team-coordinator') {
|
|
3196
|
+
var childCount = (a.childTaskIds || []).length;
|
|
3197
|
+
html += '<span style="font-size:11px;font-weight:600;padding:2px 8px;border-radius:4px;background:#7c3aed;color:#fff;">Team (' + childCount + ')</span>';
|
|
3198
|
+
} else if (a.agent) {
|
|
3199
|
+
html += '<span style="font-size:13px;color:var(--text-dim);">' + esc(a.agent) + '</span>';
|
|
3200
|
+
}
|
|
3201
|
+
html += '</div>';
|
|
3202
|
+
html += '<div class="audit-meta">';
|
|
3203
|
+
html += '<span>' + elapsed + '</span>';
|
|
3204
|
+
if (a.model) html += '<span>' + esc(a.model) + '</span>';
|
|
3205
|
+
if (a.startedAt) html += '<span>' + formatDate(a.startedAt) + '</span>';
|
|
3206
|
+
html += '</div>';
|
|
3207
|
+
html += '</div>';
|
|
3208
|
+
|
|
3209
|
+
// Task preview
|
|
3210
|
+
if (taskPreview) {
|
|
3211
|
+
html += '<div class="audit-summary"><span>' + esc(taskPreview) + '</span></div>';
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
// Expandable details (meta info, full task, parent-level output/errors)
|
|
3215
|
+
var hasDetails = a.task || a.liveOutput || a.outputPreview || a.error || a.endedAt || a.validationPassed != null;
|
|
3216
|
+
if (hasDetails) {
|
|
3217
|
+
html += '<div class="audit-events-toggle" onclick="toggleCaDetail(\\'' + detailId + '\\', this)">\\u25B6 Details</div>';
|
|
3218
|
+
html += '<div class="audit-events" id="' + detailId + '">';
|
|
3219
|
+
|
|
3220
|
+
// Meta details
|
|
3221
|
+
html += '<div class="ca-meta" style="margin-bottom:8px;">';
|
|
3222
|
+
if (a.startedAt) html += '<span>Started: ' + formatDate(a.startedAt) + '</span>';
|
|
3223
|
+
if (a.endedAt) html += '<span>Ended: ' + formatDate(a.endedAt) + '</span>';
|
|
3224
|
+
if (a.validationPassed != null) html += '<span>Tests: ' + (a.validationPassed ? 'PASS' : 'FAIL') + '</span>';
|
|
3225
|
+
if (a.workdir) html += '<span>Dir: ' + esc(a.workdir) + '</span>';
|
|
3226
|
+
html += '</div>';
|
|
3227
|
+
|
|
3228
|
+
// Full task
|
|
3229
|
+
if (a.task) html += '<div class="ca-task">' + esc(a.task) + '</div>';
|
|
3230
|
+
|
|
3231
|
+
// Output (parent-level) — skip for team coordinators since synthesis shows in tree
|
|
3232
|
+
if (!isTeam) {
|
|
3233
|
+
var output = a.liveOutput || a.outputPreview;
|
|
3234
|
+
if (output) html += '<div class="ca-output ca-output-md" style="margin-top:8px;">' + md(output) + '</div>';
|
|
3235
|
+
}
|
|
3236
|
+
if (a.error) html += '<div class="ca-output ca-error ca-output-md" style="margin-top:8px;">' + md(a.error) + '</div>';
|
|
3237
|
+
|
|
3238
|
+
html += '</div>';
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
html += '</div>'; // end .audit-entry
|
|
3242
|
+
|
|
3243
|
+
// Tree children — rendered OUTSIDE the root card, always visible
|
|
3244
|
+
if (isTeam) {
|
|
3245
|
+
html += '<div class="ca-tree-children">';
|
|
3246
|
+
html += '<div style="font-size:13px;font-weight:600;color:var(--text-dim);margin-bottom:8px;margin-top:8px;text-transform:uppercase;letter-spacing:0.04em;">Agents (' + children.length + ')</div>';
|
|
3247
|
+
|
|
3248
|
+
for (var ch = 0; ch < children.length; ch++) {
|
|
3249
|
+
var child = children[ch];
|
|
3250
|
+
var childActive = child.status === 'running' || child.status === 'validating';
|
|
3251
|
+
var childIcon = child.status === 'running' ? '\\u{1F504}' : child.status === 'completed' ? '\\u2705' : child.status === 'failed' ? '\\u274C' : child.status === 'timeout' ? '\\u23F0' : child.status === 'validating' ? '\\u{1F50D}' : '\\u2B55';
|
|
3252
|
+
var childSecs = child.durationSeconds != null ? child.durationSeconds : Math.round((Date.now() - new Date(child.startedAt).getTime()) / 1000);
|
|
3253
|
+
var childElapsed = childSecs < 60 ? childSecs + 's' : Math.floor(childSecs / 60) + 'm ' + (childSecs % 60) + 's';
|
|
3254
|
+
var childSubtask = child.subtask || child.task || '';
|
|
3255
|
+
var childDetailId = 'tm-' + (a.id || '').replace(/[^a-zA-Z0-9]/g, '') + '-' + ch;
|
|
3256
|
+
var childBorderColor = child.status === 'completed' ? 'var(--success)' : child.status === 'failed' ? 'var(--error)' : child.status === 'timeout' ? 'var(--warning)' : 'var(--highlight)';
|
|
3257
|
+
|
|
3258
|
+
html += '<div class="ca-tree-child" style="border-left:3px solid ' + childBorderColor + ';" onclick="toggleTeammate(\\'' + childDetailId + '\\')">';
|
|
3259
|
+
|
|
3260
|
+
// Header row
|
|
3261
|
+
html += '<div style="display:flex;justify-content:space-between;align-items:center;">';
|
|
3262
|
+
html += '<div style="display:flex;align-items:center;gap:6px;">';
|
|
3263
|
+
html += '<span>' + childIcon + '</span>';
|
|
3264
|
+
html += '<span class="audit-id" style="font-size:13px;">' + esc(child.id) + '</span>';
|
|
3265
|
+
if (childActive) html += '<span class="ca-spinner"></span>';
|
|
3266
|
+
html += '<span style="color:var(--text-dim);font-size:13px;">' + childElapsed + '</span>';
|
|
3267
|
+
html += '</div>';
|
|
3268
|
+
html += '<span class="ca-status-badge ' + esc(child.status) + '" style="font-size:11px;padding:2px 8px;">' + esc(child.status.toUpperCase()) + '</span>';
|
|
3269
|
+
html += '</div>';
|
|
3270
|
+
|
|
3271
|
+
// Subtask description
|
|
3272
|
+
html += '<div style="margin-top:4px;color:var(--text);">' + esc(childSubtask) + '</div>';
|
|
3273
|
+
|
|
3274
|
+
// Expandable detail (output)
|
|
3275
|
+
html += '<div id="' + childDetailId + '" style="display:none;margin-top:8px;">';
|
|
3276
|
+
var childOutput = child.liveOutput || child.outputPreview;
|
|
3277
|
+
if (childOutput) html += '<div class="ca-output ca-output-md" style="font-size:13px;max-height:300px;overflow-y:auto;">' + md(childOutput) + '</div>';
|
|
3278
|
+
if (child.error) html += '<div class="ca-output ca-error ca-output-md" style="font-size:13px;margin-top:4px;">' + md(child.error) + '</div>';
|
|
3279
|
+
html += '</div>';
|
|
3280
|
+
|
|
3281
|
+
html += '</div>'; // end .ca-tree-child
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
// Synthesis result — after children
|
|
3285
|
+
if (a.synthesisResult) {
|
|
3286
|
+
html += '<div style="margin-top:8px;padding:12px 16px;background:var(--surface-alt);border:1px solid var(--border);border-radius:8px;font-size:14px;max-height:400px;overflow-y:auto;">';
|
|
3287
|
+
html += '<div style="font-size:12px;font-weight:600;color:var(--text-dim);margin-bottom:4px;">SYNTHESIS</div>';
|
|
3288
|
+
html += '<div class="ca-output-md" style="word-break:break-word;">' + md(a.synthesisResult) + '</div>';
|
|
3289
|
+
html += '</div>';
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
html += '</div>'; // end .ca-tree-children
|
|
3293
|
+
html += '</div>'; // end .ca-tree
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
el.innerHTML = html;
|
|
3298
|
+
|
|
3299
|
+
// Restore expanded states and scroll positions
|
|
3300
|
+
expandedIds.forEach(function(id) {
|
|
3301
|
+
var detailEl = document.getElementById(id);
|
|
3302
|
+
if (detailEl) {
|
|
3303
|
+
detailEl.classList.add('expanded');
|
|
3304
|
+
var toggle = detailEl.previousElementSibling;
|
|
3305
|
+
if (toggle && toggle.classList.contains('audit-events-toggle')) {
|
|
3306
|
+
toggle.textContent = '\u25BC Details';
|
|
3307
|
+
}
|
|
3308
|
+
// Restore scroll position
|
|
3309
|
+
if (scrollPositions[id] !== undefined) {
|
|
3310
|
+
var outputEl = detailEl.querySelector('.ca-output');
|
|
3311
|
+
if (outputEl) {
|
|
3312
|
+
outputEl.scrollTop = scrollPositions[id];
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
});
|
|
3317
|
+
|
|
3318
|
+
// Restore expanded teammate panels
|
|
3319
|
+
expandedTeammates.forEach(function(tmId) {
|
|
3320
|
+
var tmEl = document.getElementById(tmId);
|
|
3321
|
+
if (tmEl) tmEl.style.display = 'block';
|
|
3322
|
+
});
|
|
3323
|
+
|
|
3324
|
+
// Auto-expand and auto-scroll live output for active agents
|
|
3325
|
+
for (var j = 0; j < agents.length; j++) {
|
|
3326
|
+
if (agents[j].status === 'running' || agents[j].status === 'validating') {
|
|
3327
|
+
// Auto-expand parent-level details
|
|
3328
|
+
var detailId2 = 'ca-detail-' + (agents[j].id || '').replace(/[^a-zA-Z0-9]/g, '');
|
|
3329
|
+
var detailEl = document.getElementById(detailId2);
|
|
3330
|
+
if (detailEl) {
|
|
3331
|
+
detailEl.classList.add('expanded');
|
|
3332
|
+
var toggle = detailEl.previousElementSibling;
|
|
3333
|
+
if (toggle && toggle.classList.contains('audit-events-toggle')) {
|
|
3334
|
+
toggle.textContent = '\\u25BC Details';
|
|
3335
|
+
}
|
|
3336
|
+
var outputEl = detailEl.querySelector('.ca-output');
|
|
3337
|
+
if (outputEl) outputEl.scrollTop = outputEl.scrollHeight;
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
// Auto-expand active children's output panels
|
|
3342
|
+
for (var k = 0; k < agents.length; k++) {
|
|
3343
|
+
if (agents[k].parentTaskId && (agents[k].status === 'running' || agents[k].status === 'validating')) {
|
|
3344
|
+
var parentId = (agents[k].parentTaskId || '').replace(/[^a-zA-Z0-9]/g, '');
|
|
3345
|
+
// Find the tm- panel for this child by scanning all panels under this parent
|
|
3346
|
+
var parentChildren = childMap[agents[k].parentTaskId] || [];
|
|
3347
|
+
for (var ci2 = 0; ci2 < parentChildren.length; ci2++) {
|
|
3348
|
+
if (parentChildren[ci2].id === agents[k].id) {
|
|
3349
|
+
var tmId = 'tm-' + parentId + '-' + ci2;
|
|
3350
|
+
var tmEl = document.getElementById(tmId);
|
|
3351
|
+
if (tmEl && tmEl.style.display === 'none') {
|
|
3352
|
+
tmEl.style.display = 'block';
|
|
3353
|
+
}
|
|
3354
|
+
if (tmEl) {
|
|
3355
|
+
var tmOutput = tmEl.querySelector('.ca-output');
|
|
3356
|
+
if (tmOutput) tmOutput.scrollTop = tmOutput.scrollHeight;
|
|
3357
|
+
}
|
|
3358
|
+
break;
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
function toggleCaDetail(id, toggleEl) {
|
|
3366
|
+
var el = document.getElementById(id);
|
|
3367
|
+
if (el) {
|
|
3368
|
+
var expanded = el.classList.toggle('expanded');
|
|
3369
|
+
if (toggleEl) {
|
|
3370
|
+
toggleEl.textContent = (expanded ? '\\u25BC' : '\\u25B6') + ' Details';
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
function toggleTeammate(id) {
|
|
3376
|
+
var el = document.getElementById(id);
|
|
3377
|
+
if (el) {
|
|
3378
|
+
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
// Stop polling when leaving the page
|
|
3383
|
+
var origOnPage = onPageActivated;
|
|
3384
|
+
onPageActivated = function(page) {
|
|
3385
|
+
if (page !== 'coding') stopCaPolling();
|
|
3386
|
+
if (page !== 'approvals') stopApprovalsPolling();
|
|
3387
|
+
origOnPage(page);
|
|
3388
|
+
};
|
|
3389
|
+
|
|
3390
|
+
// --- Audit Tab ---
|
|
3391
|
+
let auditOffset = 0;
|
|
3392
|
+
const AUDIT_PAGE_SIZE = 30;
|
|
3393
|
+
|
|
3394
|
+
function toggleAuditEvents(el, eventsId, count) {
|
|
3395
|
+
var eventsEl = document.getElementById(eventsId);
|
|
3396
|
+
var expanded = eventsEl.classList.toggle('expanded');
|
|
3397
|
+
el.textContent = (expanded ? '\\u25BC' : '\\u25B6') + ' Events (' + count + ')';
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
function formatDuration(ms) {
|
|
3401
|
+
if (ms == null) return '-';
|
|
3402
|
+
if (ms < 1000) return ms + 'ms';
|
|
3403
|
+
if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
|
|
3404
|
+
return (ms / 60000).toFixed(1) + 'm';
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
async function loadAudit(append) {
|
|
3408
|
+
if (!append) {
|
|
3409
|
+
auditOffset = 0;
|
|
3410
|
+
document.getElementById('auditEntries').innerHTML = '';
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
const triggerFilter = document.getElementById('auditTriggerFilter').value;
|
|
3414
|
+
let path = 'audit?limit=' + AUDIT_PAGE_SIZE + '&offset=' + auditOffset;
|
|
3415
|
+
if (triggerFilter) path += '&trigger=' + encodeURIComponent(triggerFilter);
|
|
3416
|
+
|
|
3417
|
+
try {
|
|
3418
|
+
const data = await api(path);
|
|
3419
|
+
const el = document.getElementById('auditEntries');
|
|
3420
|
+
const countEl = document.getElementById('auditCount');
|
|
3421
|
+
const moreBtn = document.getElementById('auditLoadMoreBtn');
|
|
3422
|
+
|
|
3423
|
+
if (!data.traces || data.traces.length === 0) {
|
|
3424
|
+
if (!append) {
|
|
3425
|
+
el.innerHTML = '<div class="empty">No audit traces found</div>';
|
|
3426
|
+
}
|
|
3427
|
+
countEl.textContent = 'Total: ' + (data.total || 0);
|
|
3428
|
+
moreBtn.style.display = 'none';
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
const html = data.traces.map(function(t) {
|
|
3433
|
+
const evtCount = t.events ? t.events.length : 0;
|
|
3434
|
+
const traceId = t.traceId || '-';
|
|
3435
|
+
const eventsId = 'evt-' + traceId.replace(/[^a-zA-Z0-9]/g, '');
|
|
3436
|
+
|
|
3437
|
+
var eventsHtml = '';
|
|
3438
|
+
if (t.events && t.events.length > 0) {
|
|
3439
|
+
eventsHtml = '<div class="audit-events-toggle" onclick="toggleAuditEvents(this, \\'' + eventsId + '\\', ' + evtCount + ')">\\u25B6 Events (' + evtCount + ')</div>' +
|
|
3440
|
+
'<div class="audit-events" id="' + eventsId + '">' +
|
|
3441
|
+
t.events.map(function(ev) {
|
|
3442
|
+
return '<div class="audit-event">' +
|
|
3443
|
+
'<span class="audit-event-type">' + esc(ev.type || '-') + '</span>' +
|
|
3444
|
+
'<span class="audit-event-summary">' + esc(ev.summary || '') + '</span>' +
|
|
3445
|
+
'<span class="audit-event-duration">' + formatDuration(ev.durationMs) + '</span>' +
|
|
3446
|
+
'</div>';
|
|
3447
|
+
}).join('') +
|
|
3448
|
+
'</div>';
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
return '<div class="audit-entry">' +
|
|
3452
|
+
'<div class="audit-header">' +
|
|
3453
|
+
'<div style="display:flex;gap:8px;align-items:center;">' +
|
|
3454
|
+
'<span class="audit-id">' + esc(traceId) + '</span>' +
|
|
3455
|
+
'<span class="audit-trigger ' + esc(t.trigger || '') + '">' + esc(t.trigger || '-') + '</span>' +
|
|
3456
|
+
'<span class="audit-badge ' + esc(t.status || '') + '">' + esc(t.status || '-') + '</span>' +
|
|
3457
|
+
'</div>' +
|
|
3458
|
+
'<div class="audit-meta">' +
|
|
3459
|
+
'<span>' + formatDuration(t.durationMs) + '</span>' +
|
|
3460
|
+
'<span>' + evtCount + ' event' + (evtCount !== 1 ? 's' : '') + '</span>' +
|
|
3461
|
+
'<span>' + formatDate(t.startedAt || t.endedAt) + '</span>' +
|
|
3462
|
+
'</div>' +
|
|
3463
|
+
'</div>' +
|
|
3464
|
+
eventsHtml +
|
|
3465
|
+
'</div>';
|
|
3466
|
+
}).join('');
|
|
3467
|
+
|
|
3468
|
+
if (append) {
|
|
3469
|
+
el.innerHTML += html;
|
|
3470
|
+
} else {
|
|
3471
|
+
el.innerHTML = html;
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
countEl.textContent = 'Showing ' + (auditOffset + data.traces.length) + ' of ' + data.total;
|
|
3475
|
+
auditOffset += data.traces.length;
|
|
3476
|
+
moreBtn.style.display = auditOffset < data.total ? '' : 'none';
|
|
3477
|
+
} catch (e) {
|
|
3478
|
+
if (!append) {
|
|
3479
|
+
document.getElementById('auditEntries').innerHTML = '<div class="empty">Failed to load audit log</div>';
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
document.getElementById('auditRefreshBtn').addEventListener('click', function() { loadAudit(false); });
|
|
3485
|
+
document.getElementById('auditTriggerFilter').addEventListener('change', function() { loadAudit(false); });
|
|
3486
|
+
document.getElementById('auditLoadMoreBtn').addEventListener('click', function() { loadAudit(true); });
|
|
3487
|
+
|
|
3488
|
+
// --- Config Tab ---
|
|
3489
|
+
async function loadConfig() {
|
|
3490
|
+
try {
|
|
3491
|
+
const data = await api('config');
|
|
3492
|
+
document.getElementById('configEditor').value = JSON.stringify(data.config, null, 2);
|
|
3493
|
+
document.getElementById('configStatus').textContent = '';
|
|
3494
|
+
} catch (e) {
|
|
3495
|
+
document.getElementById('configStatus').textContent = 'Failed to load config';
|
|
3496
|
+
document.getElementById('configStatus').style.color = 'var(--error)';
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
document.getElementById('configValidateBtn').addEventListener('click', () => {
|
|
3501
|
+
const statusEl = document.getElementById('configStatus');
|
|
3502
|
+
try {
|
|
3503
|
+
const parsed = JSON.parse(document.getElementById('configEditor').value);
|
|
3504
|
+
if (!parsed.gateway || !parsed.agents || !parsed.models || !parsed.cron) {
|
|
3505
|
+
statusEl.textContent = 'Missing required sections';
|
|
3506
|
+
statusEl.style.color = 'var(--error)';
|
|
3507
|
+
return;
|
|
3508
|
+
}
|
|
3509
|
+
statusEl.textContent = 'Valid JSON';
|
|
3510
|
+
statusEl.style.color = 'var(--success)';
|
|
3511
|
+
} catch (e) {
|
|
3512
|
+
statusEl.textContent = 'Invalid JSON: ' + e.message;
|
|
3513
|
+
statusEl.style.color = 'var(--error)';
|
|
3514
|
+
}
|
|
3515
|
+
});
|
|
3516
|
+
|
|
3517
|
+
document.getElementById('configSaveBtn').addEventListener('click', async () => {
|
|
3518
|
+
if (!confirm('Save configuration? A restart will be required for changes to take effect.')) return;
|
|
3519
|
+
const statusEl = document.getElementById('configStatus');
|
|
3520
|
+
try {
|
|
3521
|
+
const parsed = JSON.parse(document.getElementById('configEditor').value);
|
|
3522
|
+
await api('config', {
|
|
3523
|
+
method: 'PUT',
|
|
3524
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3525
|
+
body: JSON.stringify({ config: parsed })
|
|
3526
|
+
});
|
|
3527
|
+
statusEl.textContent = 'Saved';
|
|
3528
|
+
statusEl.style.color = 'var(--success)';
|
|
3529
|
+
showToast('Configuration saved');
|
|
3530
|
+
} catch (e) {
|
|
3531
|
+
statusEl.textContent = 'Save failed: ' + e.message;
|
|
3532
|
+
statusEl.style.color = 'var(--error)';
|
|
3533
|
+
showToast('Failed to save config', 'error');
|
|
3534
|
+
}
|
|
3535
|
+
});
|
|
3536
|
+
|
|
3537
|
+
// --- Agent selector helpers ---
|
|
3538
|
+
async function populateAgentSelects() {
|
|
3539
|
+
if (agentList.length > 0) return;
|
|
3540
|
+
try {
|
|
3541
|
+
const data = await api('model');
|
|
3542
|
+
if (data.agents) {
|
|
3543
|
+
agentList = Object.keys(data.agents);
|
|
3544
|
+
}
|
|
3545
|
+
} catch (e) {
|
|
3546
|
+
agentList = [];
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
[document.getElementById('memoryAgentSelect'), document.getElementById('templateAgentSelect')].forEach(sel => {
|
|
3550
|
+
if (sel.children.length > 0) return;
|
|
3551
|
+
agentList.forEach(id => {
|
|
3552
|
+
const opt = document.createElement('option');
|
|
3553
|
+
opt.value = id;
|
|
3554
|
+
opt.textContent = id;
|
|
3555
|
+
sel.appendChild(opt);
|
|
3556
|
+
});
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
document.getElementById('memoryAgentSelect').addEventListener('change', loadMemory);
|
|
3561
|
+
document.getElementById('templateAgentSelect').addEventListener('change', loadTemplates);
|
|
3562
|
+
|
|
3563
|
+
// --- Digests Tab ---
|
|
3564
|
+
async function loadDigests() {
|
|
3565
|
+
try {
|
|
3566
|
+
const data = await api('digests');
|
|
3567
|
+
const list = document.getElementById('digestList');
|
|
3568
|
+
if (!data.digests || data.digests.length === 0) {
|
|
3569
|
+
list.innerHTML = '<div class="empty">No digests found</div>';
|
|
3570
|
+
document.getElementById('digestDetail').innerHTML = '<div class="empty">Select a digest to view articles</div>';
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
list.innerHTML = data.digests.map(d =>
|
|
3574
|
+
'<div class="list-item" data-id="' + esc(d.id) + '">' +
|
|
3575
|
+
'<div class="list-item-title">' + esc(d.jobName) + '</div>' +
|
|
3576
|
+
'<div class="list-item-sub">' + d.articleCount + ' articles · ' + formatDate(d.createdAt) + '</div>' +
|
|
3577
|
+
'</div>'
|
|
3578
|
+
).join('');
|
|
3579
|
+
|
|
3580
|
+
list.querySelectorAll('.list-item').forEach(item => {
|
|
3581
|
+
item.addEventListener('click', () => loadDigestDetail(item.dataset.id));
|
|
3582
|
+
});
|
|
3583
|
+
} catch (e) {
|
|
3584
|
+
document.getElementById('digestList').innerHTML = '<div class="empty">Failed to load digests</div>';
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
function extractTitleFromUrl(url) {
|
|
3589
|
+
try {
|
|
3590
|
+
var u = new URL(url);
|
|
3591
|
+
var p = u.pathname.replace(/\\/+$/, '');
|
|
3592
|
+
var segs = p.split('/').filter(Boolean);
|
|
3593
|
+
|
|
3594
|
+
// Reddit: /r/sub/comments/id/title_slug
|
|
3595
|
+
if (u.hostname.includes('reddit.com') && segs.length >= 5) {
|
|
3596
|
+
return decodeURIComponent((segs[4] || segs[segs.length - 1]).replace(/_/g, ' '));
|
|
3597
|
+
}
|
|
3598
|
+
// GitHub: /owner/repo
|
|
3599
|
+
if (u.hostname === 'github.com' && segs.length >= 2) {
|
|
3600
|
+
return segs[0] + '/' + segs[1];
|
|
3601
|
+
}
|
|
3602
|
+
// HN
|
|
3603
|
+
if (u.hostname === 'news.ycombinator.com') {
|
|
3604
|
+
var id = u.searchParams.get('id');
|
|
3605
|
+
return id ? 'Hacker News #' + id : 'Hacker News';
|
|
3606
|
+
}
|
|
3607
|
+
// X/Twitter
|
|
3608
|
+
if (u.hostname === 'x.com' || u.hostname === 'twitter.com') {
|
|
3609
|
+
if (segs.length >= 1) return '@' + segs[0];
|
|
3610
|
+
}
|
|
3611
|
+
// General
|
|
3612
|
+
var last = segs.pop() || '';
|
|
3613
|
+
var decoded = decodeURIComponent(last.replace(/[-_]/g, ' ')).trim();
|
|
3614
|
+
return decoded.length > 0 ? decoded : u.hostname;
|
|
3615
|
+
} catch(e) {
|
|
3616
|
+
return url;
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
function formatDigestContent(text) {
|
|
3621
|
+
return renderMarkdown(text || '');
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
async function loadDigestDetail(id) {
|
|
3625
|
+
document.querySelectorAll('#digestList .list-item').forEach(i => {
|
|
3626
|
+
i.classList.toggle('active', i.dataset.id === id);
|
|
3627
|
+
});
|
|
3628
|
+
const detail = document.getElementById('digestDetail');
|
|
3629
|
+
try {
|
|
3630
|
+
const data = await api('digests/' + encodeURIComponent(id));
|
|
3631
|
+
const digest = data;
|
|
3632
|
+
|
|
3633
|
+
let html = '<div class="digest-header">';
|
|
3634
|
+
html += '<div class="digest-title">' + esc(digest.jobName) + '</div>';
|
|
3635
|
+
html += '<div class="digest-meta">' + formatDate(digest.createdAt);
|
|
3636
|
+
if (digest.articles && digest.articles.length > 0) {
|
|
3637
|
+
html += ' · ' + digest.articles.length + ' links extracted';
|
|
3638
|
+
}
|
|
3639
|
+
html += '</div></div>';
|
|
3640
|
+
|
|
3641
|
+
// Use reader view for full summaries, article cards for truncated/missing ones
|
|
3642
|
+
var hasFull = digest.summary && digest.summary.length > 1000;
|
|
3643
|
+
if (hasFull) {
|
|
3644
|
+
html += '<div class="digest-reader">' + formatDigestContent(digest.summary) + '</div>';
|
|
3645
|
+
} else if (digest.articles && digest.articles.length > 0) {
|
|
3646
|
+
html += '<div class="digest-articles">';
|
|
3647
|
+
digest.articles.forEach(function(a) {
|
|
3648
|
+
var title = a.title;
|
|
3649
|
+
// If title looks like a bare URL, extract something readable
|
|
3650
|
+
if (/^https?:\\/\\//.test(title)) {
|
|
3651
|
+
title = extractTitleFromUrl(title);
|
|
3652
|
+
}
|
|
3653
|
+
var badge = '<span class="source-badge">' + esc(a.source) + '</span>';
|
|
3654
|
+
var stats = '';
|
|
3655
|
+
if (a.score != null) stats += '⬆️ ' + a.score;
|
|
3656
|
+
if (a.comments != null) stats += (stats ? ' · ' : '') + '💬 ' + a.comments;
|
|
3657
|
+
html += '<div class="digest-article-card">' +
|
|
3658
|
+
badge +
|
|
3659
|
+
'<a href="' + esc(a.url) + '" target="_blank" rel="noopener" class="article-title">' + esc(title) + '</a>' +
|
|
3660
|
+
(stats ? '<div class="article-stats">' + stats + '</div>' : '') +
|
|
3661
|
+
'</div>';
|
|
3662
|
+
});
|
|
3663
|
+
html += '</div>';
|
|
3664
|
+
} else {
|
|
3665
|
+
html += '<div class="empty">No content in this digest</div>';
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
detail.innerHTML = html;
|
|
3669
|
+
} catch (e) {
|
|
3670
|
+
detail.innerHTML = '<div class="empty">Failed to load digest</div>';
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
|
|
3675
|
+
// --- Skills Tab ---
|
|
3676
|
+
async function loadSkills() {
|
|
3677
|
+
try {
|
|
3678
|
+
const data = await api('skills');
|
|
3679
|
+
const el = document.getElementById('skillsList');
|
|
3680
|
+
const countEl = document.getElementById('skillsCount');
|
|
3681
|
+
const skills = data.skills || [];
|
|
3682
|
+
|
|
3683
|
+
countEl.textContent = skills.length + ' skill' + (skills.length !== 1 ? 's' : '');
|
|
3684
|
+
|
|
3685
|
+
if (skills.length === 0) {
|
|
3686
|
+
el.innerHTML = '<div class="empty">No skills found. Create one or add SKILL.md files to ~/.skimpyclaw/skills/</div>';
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
el.innerHTML = skills.map(function(s) {
|
|
3691
|
+
var emoji = s.emoji || '\\u{1F527}';
|
|
3692
|
+
var statusBadge = '';
|
|
3693
|
+
if (!s.eligible) {
|
|
3694
|
+
statusBadge = '<span class="audit-badge error">\\u274C ' + esc(s.reason || 'ineligible') + '</span>';
|
|
3695
|
+
} else if (!s.enabled) {
|
|
3696
|
+
statusBadge = '<span class="audit-badge" style="background:rgba(176,124,26,0.16);color:var(--warning);">\\u26A0\\uFE0F disabled</span>';
|
|
3697
|
+
} else {
|
|
3698
|
+
statusBadge = '<span class="audit-badge ok">\\u2705 eligible</span>';
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
var tags = s.tags && s.tags.length > 0
|
|
3702
|
+
? s.tags.map(function(t) { return '<span style="background:var(--surface-alt);padding:2px 6px;border-radius:4px;font-size:11px;color:var(--text-dim);">' + esc(t) + '</span>'; }).join(' ')
|
|
3703
|
+
: '';
|
|
3704
|
+
|
|
3705
|
+
return '<div class="audit-entry" style="cursor:pointer;" data-skill="' + esc(s.name) + '">' +
|
|
3706
|
+
'<div class="audit-header">' +
|
|
3707
|
+
'<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">' +
|
|
3708
|
+
'<span style="font-size:18px;">' + emoji + '</span>' +
|
|
3709
|
+
'<span class="audit-id">' + esc(s.name) + '</span>' +
|
|
3710
|
+
statusBadge +
|
|
3711
|
+
(tags ? '<span style="display:flex;gap:4px;">' + tags + '</span>' : '') +
|
|
3712
|
+
'</div>' +
|
|
3713
|
+
'<div style="display:flex;gap:8px;align-items:center;">' +
|
|
3714
|
+
'<button class="btn btn-small" onclick="event.stopPropagation();toggleSkill(\\'' + esc(s.name) + '\\',' + (s.enabled ? 'false' : 'true') + ')">' +
|
|
3715
|
+
(s.enabled ? 'Disable' : 'Enable') +
|
|
3716
|
+
'</button>' +
|
|
3717
|
+
'<button class="btn btn-small btn-danger" onclick="event.stopPropagation();deleteSkill(\\'' + esc(s.name) + '\\')">Delete</button>' +
|
|
3718
|
+
'</div>' +
|
|
3719
|
+
'</div>' +
|
|
3720
|
+
'<div class="audit-summary"><span>' + esc(s.description || 'No description') + '</span></div>' +
|
|
3721
|
+
'</div>';
|
|
3722
|
+
}).join('');
|
|
3723
|
+
|
|
3724
|
+
el.querySelectorAll('.audit-entry[data-skill]').forEach(function(entry) {
|
|
3725
|
+
entry.addEventListener('click', function() {
|
|
3726
|
+
loadSkillDetail(entry.getAttribute('data-skill'));
|
|
3727
|
+
});
|
|
3728
|
+
});
|
|
3729
|
+
} catch (e) {
|
|
3730
|
+
document.getElementById('skillsList').innerHTML = '<div class="empty">Failed to load skills</div>';
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3734
|
+
async function loadSkillDetail(name) {
|
|
3735
|
+
var el = document.getElementById('skillDetail');
|
|
3736
|
+
try {
|
|
3737
|
+
var data = await api('skills/' + encodeURIComponent(name));
|
|
3738
|
+
el.style.display = 'block';
|
|
3739
|
+
el.innerHTML = '<div class="card">' +
|
|
3740
|
+
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">' +
|
|
3741
|
+
'<div><span style="font-size:20px;">' + (data.emoji || '\\u{1F527}') + '</span> <strong style="font-size:18px;">' + esc(data.name) + '</strong></div>' +
|
|
3742
|
+
'<button class="btn btn-small" onclick="document.getElementById(\\'skillDetail\\').style.display=\\'none\\'">Close</button>' +
|
|
3743
|
+
'</div>' +
|
|
3744
|
+
'<div style="margin-bottom:8px;color:var(--text-dim);">' + esc(data.description || '') + '</div>' +
|
|
3745
|
+
'<div style="display:flex;gap:12px;margin-bottom:12px;font-size:13px;font-family:var(--mono);color:var(--text-dim);">' +
|
|
3746
|
+
'<span>Priority: ' + (data.priority || 100) + '</span>' +
|
|
3747
|
+
'<span>Eligible: ' + (data.eligible ? 'Yes' : 'No') + '</span>' +
|
|
3748
|
+
'<span>Enabled: ' + (data.enabled ? 'Yes' : 'No') + '</span>' +
|
|
3749
|
+
'</div>' +
|
|
3750
|
+
(data.requires ? '<div style="margin-bottom:8px;font-size:13px;"><strong>Requires:</strong> <code>' + esc(JSON.stringify(data.requires)) + '</code></div>' : '') +
|
|
3751
|
+
(data.contexts ? '<div style="margin-bottom:8px;font-size:13px;"><strong>Contexts:</strong> <code>' + esc(JSON.stringify(data.contexts)) + '</code></div>' : '') +
|
|
3752
|
+
'<div style="margin-top:12px;"><strong>Content:</strong></div>' +
|
|
3753
|
+
'<pre style="white-space:pre-wrap;font-size:13px;line-height:1.6;background:var(--surface-alt);padding:12px;border-radius:8px;margin-top:6px;max-height:400px;overflow-y:auto;">' + esc(data.body || '(empty)') + '</pre>' +
|
|
3754
|
+
'</div>';
|
|
3755
|
+
} catch (e) {
|
|
3756
|
+
el.style.display = 'block';
|
|
3757
|
+
el.innerHTML = '<div class="empty">Failed to load skill details</div>';
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
async function toggleSkill(name, enabled) {
|
|
3762
|
+
try {
|
|
3763
|
+
await api('skills/' + encodeURIComponent(name), {
|
|
3764
|
+
method: 'PUT',
|
|
3765
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3766
|
+
body: JSON.stringify({ enabled: enabled })
|
|
3767
|
+
});
|
|
3768
|
+
showToast('Skill ' + name + ' ' + (enabled ? 'enabled' : 'disabled'));
|
|
3769
|
+
loadSkills();
|
|
3770
|
+
} catch (e) {
|
|
3771
|
+
showToast('Failed to update skill: ' + e.message, 'error');
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
async function deleteSkill(name) {
|
|
3776
|
+
if (!confirm('Delete skill "' + name + '"? This cannot be undone.')) return;
|
|
3777
|
+
try {
|
|
3778
|
+
await api('skills/' + encodeURIComponent(name), { method: 'DELETE' });
|
|
3779
|
+
showToast('Skill deleted: ' + name);
|
|
3780
|
+
document.getElementById('skillDetail').style.display = 'none';
|
|
3781
|
+
loadSkills();
|
|
3782
|
+
} catch (e) {
|
|
3783
|
+
showToast('Failed to delete skill: ' + e.message, 'error');
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
document.getElementById('skillsRefreshBtn').addEventListener('click', loadSkills);
|
|
3788
|
+
document.getElementById('skillsCreateBtn').addEventListener('click', function() {
|
|
3789
|
+
document.getElementById('skillsCreateForm').style.display = 'block';
|
|
3790
|
+
document.getElementById('skillNameInput').value = '';
|
|
3791
|
+
document.getElementById('skillContentInput').value = '';
|
|
3792
|
+
});
|
|
3793
|
+
document.getElementById('skillCancelCreateBtn').addEventListener('click', function() {
|
|
3794
|
+
document.getElementById('skillsCreateForm').style.display = 'none';
|
|
3795
|
+
});
|
|
3796
|
+
document.getElementById('skillSaveNewBtn').addEventListener('click', async function() {
|
|
3797
|
+
var name = document.getElementById('skillNameInput').value.trim();
|
|
3798
|
+
var content = document.getElementById('skillContentInput').value;
|
|
3799
|
+
if (!name) { showToast('Skill name required', 'error'); return; }
|
|
3800
|
+
if (!content) { showToast('Content required', 'error'); return; }
|
|
3801
|
+
try {
|
|
3802
|
+
await api('skills', {
|
|
3803
|
+
method: 'POST',
|
|
3804
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3805
|
+
body: JSON.stringify({ name: name, content: content })
|
|
3806
|
+
});
|
|
3807
|
+
showToast('Skill created: ' + name);
|
|
3808
|
+
document.getElementById('skillsCreateForm').style.display = 'none';
|
|
3809
|
+
loadSkills();
|
|
3810
|
+
} catch (e) {
|
|
3811
|
+
showToast('Failed to create skill: ' + e.message, 'error');
|
|
3812
|
+
}
|
|
3813
|
+
});
|
|
3814
|
+
|
|
3815
|
+
// --- Approvals Tab ---
|
|
3816
|
+
let approvalsInterval = null;
|
|
3817
|
+
|
|
3818
|
+
function stopApprovalsPolling() {
|
|
3819
|
+
if (approvalsInterval) {
|
|
3820
|
+
clearInterval(approvalsInterval);
|
|
3821
|
+
approvalsInterval = null;
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
function startApprovalsPolling() {
|
|
3826
|
+
loadApprovals();
|
|
3827
|
+
stopApprovalsPolling();
|
|
3828
|
+
if (document.getElementById('approvalsAutoRefresh').checked) {
|
|
3829
|
+
approvalsInterval = setInterval(loadApprovals, 5000);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
|
|
3833
|
+
function formatCountdown(expiresAt, serverNow) {
|
|
3834
|
+
var now = serverNow ? new Date(serverNow).getTime() : Date.now();
|
|
3835
|
+
var exp = new Date(expiresAt).getTime();
|
|
3836
|
+
var remaining = Math.max(0, Math.round((exp - now) / 1000));
|
|
3837
|
+
if (remaining <= 0) return '<span class="approval-countdown">expired</span>';
|
|
3838
|
+
var m = Math.floor(remaining / 60);
|
|
3839
|
+
var s = remaining % 60;
|
|
3840
|
+
var cls = remaining < 60 ? 'approval-countdown urgent' : 'approval-countdown';
|
|
3841
|
+
return '<span class="' + cls + '">' + m + ':' + (s < 10 ? '0' : '') + s + '</span>';
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
async function loadApprovals() {
|
|
3845
|
+
try {
|
|
3846
|
+
var data = await api('approvals');
|
|
3847
|
+
var pending = data.pending || [];
|
|
3848
|
+
var recent = (data.recent || []).filter(function(a) { return a.status !== 'pending'; });
|
|
3849
|
+
var serverNow = data.now;
|
|
3850
|
+
|
|
3851
|
+
var pendingEl = document.getElementById('approvalsPending');
|
|
3852
|
+
var recentEl = document.getElementById('approvalsRecent');
|
|
3853
|
+
var countEl = document.getElementById('approvalsCount');
|
|
3854
|
+
|
|
3855
|
+
countEl.textContent = pending.length + ' pending';
|
|
3856
|
+
|
|
3857
|
+
if (pending.length === 0) {
|
|
3858
|
+
pendingEl.innerHTML = '<div class="empty">No pending approvals</div>';
|
|
3859
|
+
} else {
|
|
3860
|
+
var html = '';
|
|
3861
|
+
for (var i = 0; i < pending.length; i++) {
|
|
3862
|
+
var a = pending[i];
|
|
3863
|
+
html += '<div class="audit-entry">';
|
|
3864
|
+
html += '<div class="audit-header">';
|
|
3865
|
+
html += '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">';
|
|
3866
|
+
html += '<span class="audit-id">' + esc(a.id) + '</span>';
|
|
3867
|
+
html += '<span class="approval-tier tier-' + a.tier + '">TIER ' + a.tier + '</span>';
|
|
3868
|
+
html += '<span class="approval-status pending">PENDING</span>';
|
|
3869
|
+
html += formatCountdown(a.expiresAt, serverNow);
|
|
3870
|
+
html += '</div>';
|
|
3871
|
+
html += '<div class="audit-meta">';
|
|
3872
|
+
html += '<span>' + esc(a.reason) + '</span>';
|
|
3873
|
+
if (a.cwd) html += '<span>' + esc(a.cwd) + '</span>';
|
|
3874
|
+
html += '<span>' + formatDate(a.createdAt) + '</span>';
|
|
3875
|
+
html += '</div>';
|
|
3876
|
+
html += '</div>';
|
|
3877
|
+
html += '<div class="approval-command">' + esc(a.command) + '</div>';
|
|
3878
|
+
html += '<div class="approval-actions">';
|
|
3879
|
+
html += '<button class="btn btn-success btn-small" onclick="approveApproval('' + esc(a.id) + '')">Approve</button>';
|
|
3880
|
+
html += '<button class="btn btn-small" style="background:var(--error);color:#fff;border-color:var(--error);" onclick="denyApproval('' + esc(a.id) + '')">Deny</button>';
|
|
3881
|
+
html += '</div>';
|
|
3882
|
+
html += '</div>';
|
|
3883
|
+
}
|
|
3884
|
+
pendingEl.innerHTML = html;
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
if (recent.length === 0) {
|
|
3888
|
+
recentEl.innerHTML = '<div class="empty">No recent approvals</div>';
|
|
3889
|
+
} else {
|
|
3890
|
+
var rhtml = '';
|
|
3891
|
+
for (var j = 0; j < recent.length; j++) {
|
|
3892
|
+
var r = recent[j];
|
|
3893
|
+
rhtml += '<div class="audit-entry">';
|
|
3894
|
+
rhtml += '<div class="audit-header">';
|
|
3895
|
+
rhtml += '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">';
|
|
3896
|
+
rhtml += '<span class="audit-id">' + esc(r.id) + '</span>';
|
|
3897
|
+
rhtml += '<span class="approval-tier tier-' + r.tier + '">TIER ' + r.tier + '</span>';
|
|
3898
|
+
rhtml += '<span class="approval-status ' + esc(r.status) + '">' + esc(r.status).toUpperCase() + '</span>';
|
|
3899
|
+
rhtml += '</div>';
|
|
3900
|
+
rhtml += '<div class="audit-meta">';
|
|
3901
|
+
rhtml += '<span>' + esc(r.reason) + '</span>';
|
|
3902
|
+
if (r.resolvedAt) rhtml += '<span>' + formatDate(r.resolvedAt) + '</span>';
|
|
3903
|
+
if (r.approvedBy) rhtml += '<span>by ' + esc(r.approvedBy) + '</span>';
|
|
3904
|
+
if (r.deniedBy) rhtml += '<span>by ' + esc(r.deniedBy) + '</span>';
|
|
3905
|
+
rhtml += '</div>';
|
|
3906
|
+
rhtml += '</div>';
|
|
3907
|
+
rhtml += '<div class="approval-command">' + esc(r.command) + '</div>';
|
|
3908
|
+
rhtml += '</div>';
|
|
3909
|
+
}
|
|
3910
|
+
recentEl.innerHTML = rhtml;
|
|
3911
|
+
}
|
|
3912
|
+
} catch (e) {
|
|
3913
|
+
document.getElementById('approvalsPending').innerHTML = '<div class="empty">Failed to load approvals</div>';
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
async function approveApproval(id) {
|
|
3918
|
+
try {
|
|
3919
|
+
await api('approvals/' + id + '/approve', { method: 'POST' });
|
|
3920
|
+
showToast('Approved: ' + id);
|
|
3921
|
+
loadApprovals();
|
|
3922
|
+
} catch (e) {
|
|
3923
|
+
showToast('Failed to approve: ' + e.message, 'error');
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
async function denyApproval(id) {
|
|
3928
|
+
try {
|
|
3929
|
+
await api('approvals/' + id + '/deny', { method: 'POST' });
|
|
3930
|
+
showToast('Denied: ' + id);
|
|
3931
|
+
loadApprovals();
|
|
3932
|
+
} catch (e) {
|
|
3933
|
+
showToast('Failed to deny: ' + e.message, 'error');
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
document.getElementById('approvalsRefreshBtn').addEventListener('click', function() { loadApprovals(); });
|
|
3938
|
+
document.getElementById('approvalsAutoRefresh').addEventListener('change', function() {
|
|
3939
|
+
if (this.checked) {
|
|
3940
|
+
startApprovalsPolling();
|
|
3941
|
+
} else {
|
|
3942
|
+
stopApprovalsPolling();
|
|
3943
|
+
}
|
|
3944
|
+
});
|
|
3945
|
+
|
|
3946
|
+
// --- Health Tab (unified: diagnostics + environment + features) ---
|
|
3947
|
+
const DOCTOR_CATEGORY_LABELS = {
|
|
3948
|
+
environment: 'Environment',
|
|
3949
|
+
configuration: 'Configuration',
|
|
3950
|
+
provider_auth: 'Provider Auth',
|
|
3951
|
+
channels: 'Channels',
|
|
3952
|
+
runtime: 'Runtime',
|
|
3953
|
+
};
|
|
3954
|
+
const DOCTOR_CATEGORY_ORDER = ['environment', 'configuration', 'provider_auth', 'channels', 'runtime'];
|
|
3955
|
+
|
|
3956
|
+
async function loadHealth() {
|
|
3957
|
+
const summaryEl = document.getElementById('doctorSummary');
|
|
3958
|
+
const categoriesEl = document.getElementById('doctorCategories');
|
|
3959
|
+
const tsEl = document.getElementById('doctorTimestamp');
|
|
3960
|
+
const envEl = document.getElementById('healthEnvVars');
|
|
3961
|
+
const featEl = document.getElementById('healthFeatures');
|
|
3962
|
+
try {
|
|
3963
|
+
const [doctorData, healthData] = await Promise.all([api('doctor'), api('health')]);
|
|
3964
|
+
const report = doctorData.report;
|
|
3965
|
+
|
|
3966
|
+
// Timestamp
|
|
3967
|
+
if (tsEl) {
|
|
3968
|
+
const started = new Date(report.startedAt);
|
|
3969
|
+
const finished = new Date(report.finishedAt);
|
|
3970
|
+
const durationMs = finished - started;
|
|
3971
|
+
tsEl.textContent = 'Ran ' + finished.toLocaleTimeString() + ' (' + durationMs + 'ms)';
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
// Summary banner
|
|
3975
|
+
const total = report.checks.length;
|
|
3976
|
+
const passed = report.checks.filter(c => c.ok).length;
|
|
3977
|
+
const failed = report.checks.filter(c => !c.ok && c.fatal).length;
|
|
3978
|
+
const warned = report.checks.filter(c => !c.ok && !c.fatal).length;
|
|
3979
|
+
const overallColor = report.ok ? 'var(--success)' : (failed > 0 ? 'var(--error)' : 'var(--warning, #f59e0b)');
|
|
3980
|
+
const overallLabel = report.ok ? 'All checks passed' : (failed > 0 ? 'Fatal issues found' : 'Warnings detected');
|
|
3981
|
+
summaryEl.innerHTML =
|
|
3982
|
+
'<div style="display:flex;gap:24px;align-items:center;padding:8px 0;">' +
|
|
3983
|
+
'<span style="font-size:18px;font-weight:700;color:' + overallColor + ';">' + esc(overallLabel) + '</span>' +
|
|
3984
|
+
'<span style="font-size:13px;color:var(--text-dim);">' +
|
|
3985
|
+
'<span style="color:var(--success);font-weight:600;">' + passed + '</span> pass · ' +
|
|
3986
|
+
(warned > 0 ? '<span style="color:var(--warning, #f59e0b);font-weight:600;">' + warned + '</span> warn · ' : '') +
|
|
3987
|
+
(failed > 0 ? '<span style="color:var(--error);font-weight:600;">' + failed + '</span> fail · ' : '') +
|
|
3988
|
+
total + ' total' +
|
|
3989
|
+
'</span>' +
|
|
3990
|
+
'<span style="font-size:12px;color:var(--text-dim);">exit ' + report.exitCode + '</span>' +
|
|
3991
|
+
'</div>';
|
|
3992
|
+
|
|
3993
|
+
// Group checks by category
|
|
3994
|
+
const grouped = {};
|
|
3995
|
+
for (const cat of DOCTOR_CATEGORY_ORDER) grouped[cat] = [];
|
|
3996
|
+
for (const ch of report.checks) {
|
|
3997
|
+
if (!grouped[ch.category]) grouped[ch.category] = [];
|
|
3998
|
+
grouped[ch.category].push(ch);
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
let html = '';
|
|
4002
|
+
for (const cat of DOCTOR_CATEGORY_ORDER) {
|
|
4003
|
+
const checks = grouped[cat];
|
|
4004
|
+
if (!checks || checks.length === 0) continue;
|
|
4005
|
+
const catLabel = DOCTOR_CATEGORY_LABELS[cat] || cat;
|
|
4006
|
+
const catPassed = checks.every(c => c.ok);
|
|
4007
|
+
const catIcon = catPassed ? '<span style="color:var(--success);">●</span>' : '<span style="color:var(--error);">●</span>';
|
|
4008
|
+
html += '<div class="card" style="margin-bottom:12px;">';
|
|
4009
|
+
html += '<div class="card-title">' + catIcon + ' ' + esc(catLabel) + '</div>';
|
|
4010
|
+
html += '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
|
|
4011
|
+
html += '<tr style="border-bottom:1px solid var(--border);">' +
|
|
4012
|
+
'<th style="text-align:left;padding:6px;">Check</th>' +
|
|
4013
|
+
'<th style="text-align:left;padding:6px;width:60px;">Status</th>' +
|
|
4014
|
+
'<th style="text-align:left;padding:6px;">Detail</th>' +
|
|
4015
|
+
'<th style="text-align:left;padding:6px;">Remedy</th>' +
|
|
4016
|
+
'</tr>';
|
|
4017
|
+
for (const ch of checks) {
|
|
4018
|
+
const isFatal = !ch.ok && ch.fatal;
|
|
4019
|
+
const color = ch.ok ? 'var(--success)' : 'var(--error)';
|
|
4020
|
+
const status = ch.ok ? 'PASS' : (isFatal ? 'FAIL' : 'WARN');
|
|
4021
|
+
html += '<tr style="border-bottom:1px solid var(--border);">';
|
|
4022
|
+
html += '<td style="padding:6px;font-family:var(--mono);font-size:12px;">' + esc(ch.name) + '</td>';
|
|
4023
|
+
html += '<td style="padding:6px;color:' + color + ';font-weight:600;">' + status + '</td>';
|
|
4024
|
+
html += '<td style="padding:6px;color:var(--text-dim);font-size:12px;">' + esc(ch.detail) + '</td>';
|
|
4025
|
+
html += '<td style="padding:6px;color:var(--text-dim);font-size:12px;">' + (ch.remedy ? esc(ch.remedy) : '') + '</td>';
|
|
4026
|
+
html += '</tr>';
|
|
4027
|
+
}
|
|
4028
|
+
html += '</table></div>';
|
|
4029
|
+
}
|
|
4030
|
+
categoriesEl.innerHTML = html;
|
|
4031
|
+
|
|
4032
|
+
// Render env vars
|
|
4033
|
+
if (healthData.envVars && healthData.envVars.length > 0) {
|
|
4034
|
+
let envHtml = '<div style="font-size:13px;">';
|
|
4035
|
+
for (const ev of healthData.envVars) {
|
|
4036
|
+
const icon = ev.set ? '<span style="color:var(--success);">●</span>' : '<span style="color:var(--error);">○</span>';
|
|
4037
|
+
const label = ev.set ? 'set' : 'missing';
|
|
4038
|
+
envHtml += '<div style="padding:4px 0;display:flex;justify-content:space-between;border-bottom:1px solid var(--border);">';
|
|
4039
|
+
envHtml += '<span style="font-family:var(--mono);font-size:12px;">' + icon + ' ' + esc(ev.name) + '</span>';
|
|
4040
|
+
envHtml += '<span style="color:var(--text-dim);font-size:12px;">' + label + '</span>';
|
|
4041
|
+
envHtml += '</div>';
|
|
4042
|
+
}
|
|
4043
|
+
envHtml += '</div>';
|
|
4044
|
+
envEl.innerHTML = envHtml;
|
|
4045
|
+
} else {
|
|
4046
|
+
envEl.innerHTML = '<span style="color:var(--text-dim);">No env var references found</span>';
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
// Render features
|
|
4050
|
+
if (healthData.features) {
|
|
4051
|
+
let featHtml = '<div style="font-size:13px;">';
|
|
4052
|
+
for (const [name, enabled] of Object.entries(healthData.features)) {
|
|
4053
|
+
const icon = enabled ? '<span style="color:var(--success);">✓</span>' : '<span style="color:var(--text-dim);">✗</span>';
|
|
4054
|
+
featHtml += '<div style="padding:4px 0;border-bottom:1px solid var(--border);">' + icon + ' ' + esc(name) + '</div>';
|
|
4055
|
+
}
|
|
4056
|
+
featHtml += '</div>';
|
|
4057
|
+
featEl.innerHTML = featHtml;
|
|
4058
|
+
}
|
|
4059
|
+
} catch (err) {
|
|
4060
|
+
summaryEl.innerHTML = '<span style="color:var(--error);">Failed to load health data: ' + esc(err.message) + '</span>';
|
|
4061
|
+
categoriesEl.innerHTML = '';
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
document.getElementById('healthRecheckBtn')?.addEventListener('click', () => loadHealth());
|
|
4066
|
+
|
|
4067
|
+
// --- Init ---
|
|
4068
|
+
switchPage('overview');
|
|
4069
|
+
</script>
|
|
4070
|
+
</body>
|
|
4071
|
+
</html>`;
|