@hongmaple0820/scale-engine 0.50.1 → 0.50.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/api/http.js +3 -1
- package/dist/api/http.js.map +1 -1
- package/dist/cli/cortexCommands.d.ts +16 -0
- package/dist/cli/cortexCommands.js +47 -4
- package/dist/cli/cortexCommands.js.map +1 -1
- package/dist/cortex/InstinctStore.d.ts +13 -1
- package/dist/cortex/InstinctStore.js +90 -11
- package/dist/cortex/InstinctStore.js.map +1 -1
- package/dist/cortex/SessionInjector.js +39 -2
- package/dist/cortex/SessionInjector.js.map +1 -1
- package/dist/dashboard/DashboardServer.d.ts +158 -0
- package/dist/dashboard/DashboardServer.js +753 -13
- package/dist/dashboard/DashboardServer.js.map +1 -1
- package/dist/dashboard/spa/assets/index-VYBCLBje.js +11 -0
- package/dist/dashboard/spa/assets/index-VhwY_ac1.css +1 -0
- package/dist/dashboard/spa/assets/naive-ui-BQy2AJkt.js +3340 -0
- package/dist/dashboard/spa/assets/vendor-BPU6aOYA.js +3 -0
- package/dist/dashboard/spa/assets/vue-CQQMb5Wi.js +17 -0
- package/dist/dashboard/spa/index.html +15 -462
- package/dist/memory/MemoryFabric.d.ts +13 -1
- package/dist/memory/MemoryFabric.js +60 -0
- package/dist/memory/MemoryFabric.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/workflow/ASSESSMENT_INDEX.md +326 -0
- package/docs/workflow/COMPARATIVE_ANALYSIS.md +422 -0
- package/docs/workflow/EXECUTIVE_SUMMARY.md +310 -0
- package/docs/workflow/IMPROVEMENT_CHECKLIST.md +518 -0
- package/docs/workflow/IMPROVEMENT_ROADMAP.md +707 -0
- package/docs/workflow/README.md +8 -0
- package/package.json +6 -2
- package/dist/dashboard/spa/app.js +0 -515
- package/dist/dashboard/spa/components/DataTable.js +0 -53
- package/dist/dashboard/spa/components/EventStream.js +0 -66
- package/dist/dashboard/spa/components/LoadingState.js +0 -39
- package/dist/dashboard/spa/components/MetricCard.js +0 -30
- package/dist/dashboard/spa/components/Panel.js +0 -27
- package/dist/dashboard/spa/components/StatusBadge.js +0 -51
- package/dist/dashboard/spa/i18n.js +0 -767
- package/dist/dashboard/spa/pages/costs.js +0 -522
- package/dist/dashboard/spa/pages/documents.js +0 -540
- package/dist/dashboard/spa/pages/knowledge.js +0 -457
- package/dist/dashboard/spa/pages/monitoring.js +0 -361
- package/dist/dashboard/spa/pages/overview.js +0 -301
- package/dist/dashboard/spa/pages/topology-renderers.js +0 -251
- package/dist/dashboard/spa/pages/topology.js +0 -370
- package/dist/dashboard/spa/pages/workflow-renderers.js +0 -239
- package/dist/dashboard/spa/pages/workflow.js +0 -217
package/docs/workflow/README.md
CHANGED
|
@@ -21,8 +21,12 @@ make verify PROFILE=default
|
|
|
21
21
|
scale gates status --json
|
|
22
22
|
scale score task --changed --json
|
|
23
23
|
scale prompt optimize --input "raw coding request" --json
|
|
24
|
+
scale vibe-index
|
|
25
|
+
npm run serve
|
|
24
26
|
```
|
|
25
27
|
|
|
28
|
+
`npm run build` now builds the TypeScript CLI/runtime and builds the Vue 3 dashboard as the default UI. `npm run serve` prints the concrete dashboard URL, normally `http://localhost:3210/`. If the port is already occupied, either stop the stale process or set `SCALE_DASHBOARD_PORT=auto`.
|
|
29
|
+
|
|
26
30
|
### SCALE 2.0 引擎命令
|
|
27
31
|
|
|
28
32
|
```bash
|
|
@@ -52,6 +56,10 @@ See [GATES_AND_SCORE.md](GATES_AND_SCORE.md) for gate catalog visibility, archit
|
|
|
52
56
|
|
|
53
57
|
See [PROMPT_OPTIMIZATION.md](PROMPT_OPTIMIZATION.md) for the deterministic prompt rewrite layer used by `scale prompt optimize` and `scale define`.
|
|
54
58
|
|
|
59
|
+
See [../VIBE-TEMPLATES.md](../VIBE-TEMPLATES.md) for built-in vibe coding templates. The default live dashboard is the Vue 3 + Naive UI app at the server root `/`. The Vue dashboard includes Overview, Workflow, Topology, Monitoring, Token/Cost, Documents, Knowledge, and Prompt Studio pages. Prompt Studio covers templates, packs, custom prompts, copy/download/export, and the deterministic optimizer. The Knowledge page separates repo knowledge base, gbrain memory, and graph visualization instead of treating memory as the whole knowledge system.
|
|
60
|
+
|
|
61
|
+
The dashboard reads `GET /api/dashboard/capabilities` before rendering capability claims. Empty panels should have an explicit source and reason: missing model usage means no `.scale/model-usage/usage.jsonl`; missing knowledge base means no knowledge docs, `.scale/knowledge.db`, or `graphify-out/graph.json`; missing gbrain memory means no `.scale/memory/brain.sqlite` nodes; partial realtime/workflow transitions mean the HTTP serve path has not been started with EventBus/FSM/store injection.
|
|
62
|
+
|
|
55
63
|
## 模板与示例
|
|
56
64
|
|
|
57
65
|
- 不知道一个任务该用哪些模板、模板和哪个门禁挂钩,先读 [TEMPLATE_GUIDE.md](TEMPLATE_GUIDE.md)(按等级 + 按改动类型的选择矩阵,含模板↔门禁映射)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hongmaple0820/scale-engine",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.2",
|
|
4
4
|
"description": "Executable AI agent governance with workflow gates, evidence, skill/tool orchestration, and traceable HTML artifacts",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
-
"build": "tsc && node scripts/workflow/
|
|
44
|
+
"build": "tsc && node scripts/workflow/cleanup-dashboard-legacy.mjs && vite build --config vite.dashboard.config.ts",
|
|
45
45
|
"dev": "bun --watch src/api/cli.ts",
|
|
46
46
|
"test": "node scripts/workflow/run-vitest.mjs --timeout-ms 900000 --testTimeout=120000",
|
|
47
47
|
"test:serial": "vitest run --reporter dot --pool=forks --poolOptions.forks.maxForks=1 --poolOptions.forks.minForks=1",
|
|
@@ -68,9 +68,11 @@
|
|
|
68
68
|
"execa": "^9.3.0",
|
|
69
69
|
"hono": "^4.5.0",
|
|
70
70
|
"js-yaml": "^4.1.0",
|
|
71
|
+
"naive-ui": "^2.44.1",
|
|
71
72
|
"pino": "^9.3.0",
|
|
72
73
|
"pino-pretty": "^11.2.0",
|
|
73
74
|
"type-is": "2.0.1",
|
|
75
|
+
"vue": "^3.5.38",
|
|
74
76
|
"zod": "^3.23.0"
|
|
75
77
|
},
|
|
76
78
|
"overrides": {
|
|
@@ -87,10 +89,12 @@
|
|
|
87
89
|
"@types/node": "^20.14.0",
|
|
88
90
|
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
|
89
91
|
"@typescript-eslint/parser": "^8.59.3",
|
|
92
|
+
"@vitejs/plugin-vue": "^5.2.4",
|
|
90
93
|
"@vitest/coverage-v8": "2.1.9",
|
|
91
94
|
"eslint": "^9.0.0",
|
|
92
95
|
"tsx": "^4.21.0",
|
|
93
96
|
"typescript": "^5.9.3",
|
|
97
|
+
"vite": "^5.4.21",
|
|
94
98
|
"vitest": "^2.0.0"
|
|
95
99
|
},
|
|
96
100
|
"engines": {
|
|
@@ -1,515 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SCALE Engine Dashboard 2.0 — SPA Core
|
|
3
|
-
* Client-side routing, theme management, SSE, i18n, shared utilities
|
|
4
|
-
*/
|
|
5
|
-
;(() => {
|
|
6
|
-
'use strict'
|
|
7
|
-
|
|
8
|
-
// ── Utilities ──────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
const $ = (sel, ctx = document) => ctx.querySelector(sel)
|
|
11
|
-
const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)]
|
|
12
|
-
|
|
13
|
-
function el(tag, options = {}, children = []) {
|
|
14
|
-
const node = document.createElement(tag)
|
|
15
|
-
if (options.id) node.id = options.id
|
|
16
|
-
if (options.className) node.className = options.className
|
|
17
|
-
if (options.text != null) node.textContent = String(options.text)
|
|
18
|
-
if (options.type != null) node.type = String(options.type)
|
|
19
|
-
if (options.value != null) node.value = String(options.value)
|
|
20
|
-
if (options.placeholder != null) node.placeholder = String(options.placeholder)
|
|
21
|
-
if (options.title != null) node.title = String(options.title)
|
|
22
|
-
if (options.disabled != null) node.disabled = Boolean(options.disabled)
|
|
23
|
-
if (options.checked != null) node.checked = Boolean(options.checked)
|
|
24
|
-
if (options.dataset) {
|
|
25
|
-
for (const [key, value] of Object.entries(options.dataset)) node.dataset[key] = String(value)
|
|
26
|
-
}
|
|
27
|
-
if (options.attrs) {
|
|
28
|
-
for (const [key, value] of Object.entries(options.attrs)) {
|
|
29
|
-
if (value == null || value === false) continue
|
|
30
|
-
node.setAttribute(key, value === true ? '' : String(value))
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (options.style) Object.assign(node.style, options.style)
|
|
34
|
-
for (const child of children) {
|
|
35
|
-
if (child == null) continue
|
|
36
|
-
node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child)
|
|
37
|
-
}
|
|
38
|
-
return node
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function safeClassToken(value) {
|
|
42
|
-
return String(value ?? '').replace(/[^a-zA-Z0-9_-]/g, '-')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function textBlock(message, className = 'text-muted text-sm') {
|
|
46
|
-
return el('div', { className, text: message })
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function renderText(container, message, className = 'text-muted text-sm') {
|
|
50
|
-
container.replaceChildren(textBlock(message, className))
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function copyText(text, button, options = {}) {
|
|
54
|
-
await navigator.clipboard.writeText(String(text ?? ''))
|
|
55
|
-
if (!button) return
|
|
56
|
-
const original = button.textContent
|
|
57
|
-
button.textContent = options.copiedLabel ?? t('common.copied')
|
|
58
|
-
setTimeout(() => { button.textContent = original }, options.resetMs ?? 1500)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function downloadText(name, text, type = 'text/plain;charset=utf-8') {
|
|
62
|
-
const url = URL.createObjectURL(new Blob([String(text ?? '')], { type }))
|
|
63
|
-
const link = el('a', { attrs: { href: url, download: name } })
|
|
64
|
-
document.body.appendChild(link)
|
|
65
|
-
link.click()
|
|
66
|
-
link.remove()
|
|
67
|
-
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function dataNote(items) {
|
|
71
|
-
return el('div', { className: 'data-note' }, items.map(item => {
|
|
72
|
-
if (typeof item === 'string') return el('span', { text: item })
|
|
73
|
-
return el(item.strong ? 'strong' : 'span', { text: item.text })
|
|
74
|
-
}))
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
let pageCleanup = null
|
|
78
|
-
|
|
79
|
-
function setPageCleanup(cleanup) {
|
|
80
|
-
if (pageCleanup) {
|
|
81
|
-
try {
|
|
82
|
-
pageCleanup()
|
|
83
|
-
} catch (error) {
|
|
84
|
-
observeRecoverableError(error)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
pageCleanup = typeof cleanup === 'function' ? cleanup : null
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function autoRefreshControl(onRefresh, options = {}) {
|
|
91
|
-
const intervalMs = options.intervalMs ?? 30000
|
|
92
|
-
const status = el('span', { className: 'text-muted text-sm', text: t('common.autoRefreshOff') })
|
|
93
|
-
const checkbox = el('input', { type: 'checkbox', title: t('common.autoRefresh') })
|
|
94
|
-
const label = el('label', { className: 'field-label auto-refresh-control', title: t('common.autoRefreshHint') }, [
|
|
95
|
-
checkbox,
|
|
96
|
-
el('span', { text: t('common.autoRefresh') }),
|
|
97
|
-
status,
|
|
98
|
-
])
|
|
99
|
-
let timer = null
|
|
100
|
-
let inFlight = false
|
|
101
|
-
|
|
102
|
-
async function tick() {
|
|
103
|
-
if (inFlight) return
|
|
104
|
-
inFlight = true
|
|
105
|
-
status.textContent = t('common.refreshing')
|
|
106
|
-
try {
|
|
107
|
-
await onRefresh({ auto: true })
|
|
108
|
-
status.textContent = `${t('common.lastAutoRefresh')}: ${formatTime(Date.now())}`
|
|
109
|
-
} catch (error) {
|
|
110
|
-
observeRecoverableError(error)
|
|
111
|
-
status.textContent = t('common.failed')
|
|
112
|
-
} finally {
|
|
113
|
-
inFlight = false
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function stop() {
|
|
118
|
-
if (timer) clearInterval(timer)
|
|
119
|
-
timer = null
|
|
120
|
-
checkbox.checked = false
|
|
121
|
-
status.textContent = t('common.autoRefreshOff')
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
checkbox.addEventListener('change', () => {
|
|
125
|
-
if (!checkbox.checked) {
|
|
126
|
-
stop()
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
status.textContent = t('common.autoRefreshOn')
|
|
130
|
-
timer = setInterval(tick, intervalMs)
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
setPageCleanup(stop)
|
|
134
|
-
return label
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function emptyState(message, icon) {
|
|
138
|
-
const children = []
|
|
139
|
-
if (icon) children.push(el('div', { className: 'icon', text: icon }))
|
|
140
|
-
children.push(el('p', { text: message }))
|
|
141
|
-
return el('div', { className: 'empty-state' }, children)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function metricCard(label, value, cls = '') {
|
|
145
|
-
const valueClass = ['metric-value', cls].filter(Boolean).join(' ')
|
|
146
|
-
return el('div', { className: 'metric-card' }, [
|
|
147
|
-
el('div', { className: 'metric-label', text: label }),
|
|
148
|
-
el('div', { className: valueClass, text: value }),
|
|
149
|
-
])
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function chartContainer(title, chartId) {
|
|
153
|
-
return el('div', { className: 'chart-container' }, [
|
|
154
|
-
el('div', { className: 'chart-header' }, [
|
|
155
|
-
el('span', { className: 'chart-title', text: title }),
|
|
156
|
-
]),
|
|
157
|
-
el('div', { className: 'chart-area', id: chartId }),
|
|
158
|
-
])
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function panel(title, bodyId, options = {}) {
|
|
162
|
-
const titleChildren = [document.createTextNode(title)]
|
|
163
|
-
if (options.titleSuffix) titleChildren.push(document.createTextNode(' '), options.titleSuffix)
|
|
164
|
-
return el('div', { className: ['panel', options.className].filter(Boolean).join(' ') }, [
|
|
165
|
-
el('div', { className: 'panel-title' }, titleChildren),
|
|
166
|
-
el('div', { id: bodyId, className: options.bodyClassName ?? '' }),
|
|
167
|
-
])
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function dataTable(headers, rows) {
|
|
171
|
-
return el('table', { className: 'data-table' }, [
|
|
172
|
-
el('thead', {}, [
|
|
173
|
-
el('tr', {}, headers.map(header => el('th', { text: header }))),
|
|
174
|
-
]),
|
|
175
|
-
el('tbody', {}, rows),
|
|
176
|
-
])
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function observeRecoverableError(error) {
|
|
180
|
-
void error
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function formatNumber(n) {
|
|
184
|
-
if (n == null) return '0'
|
|
185
|
-
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M'
|
|
186
|
-
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'
|
|
187
|
-
return String(n)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function formatTime(ts) {
|
|
191
|
-
if (!ts) return '-'
|
|
192
|
-
const d = new Date(ts)
|
|
193
|
-
return d.toLocaleString()
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function relativeTime(ts) {
|
|
197
|
-
return window.I18n?.relativeTime(ts) || fallbackRelativeTime(ts)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function fallbackRelativeTime(ts) {
|
|
201
|
-
if (!ts) return '-'
|
|
202
|
-
const diff = Date.now() - ts
|
|
203
|
-
if (diff < 60000) return 'just now'
|
|
204
|
-
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago'
|
|
205
|
-
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago'
|
|
206
|
-
return Math.floor(diff / 86400000) + 'd ago'
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function fetchJSON(url) {
|
|
210
|
-
try {
|
|
211
|
-
const res = await fetch(url)
|
|
212
|
-
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
|
213
|
-
return await res.json()
|
|
214
|
-
} catch (e) {
|
|
215
|
-
observeRecoverableError(e)
|
|
216
|
-
return null
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function errorMessage(error) {
|
|
221
|
-
return error instanceof Error ? error.message : String(error || 'Unknown error')
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function renderLoading(container, message = t('common.loading')) {
|
|
225
|
-
container.replaceChildren(el('div', { className: 'loading-placeholder', text: message }))
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function renderEmptyState(container, message, options = {}) {
|
|
229
|
-
container.replaceChildren(emptyState(message, options.icon))
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ── i18n ───────────────────────────────────────────────────────────
|
|
233
|
-
|
|
234
|
-
function t(key, params) {
|
|
235
|
-
return window.I18n?.t(key, params) || key
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function runtimeLabel(scope, value) {
|
|
239
|
-
if (value == null || value === '') return '-'
|
|
240
|
-
const normalized = String(value).trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
|
|
241
|
-
if (!normalized) return '-'
|
|
242
|
-
const key = `runtime.${scope}.${normalized}`
|
|
243
|
-
const translated = t(key)
|
|
244
|
-
return translated === key ? humanizeRuntimeValue(value) : translated
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function humanizeRuntimeValue(value) {
|
|
248
|
-
return String(value ?? '')
|
|
249
|
-
.replace(/[_-]+/g, ' ')
|
|
250
|
-
.replace(/\s+/g, ' ')
|
|
251
|
-
.trim()
|
|
252
|
-
.toLowerCase()
|
|
253
|
-
.replace(/\b\w/g, c => c.toUpperCase())
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function translateDocument() {
|
|
257
|
-
$$('[data-i18n]').forEach(el => {
|
|
258
|
-
el.textContent = t(el.dataset.i18n)
|
|
259
|
-
})
|
|
260
|
-
$$('[data-i18n-placeholder]').forEach(el => {
|
|
261
|
-
el.placeholder = t(el.dataset.i18nPlaceholder)
|
|
262
|
-
})
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function updateLangToggle() {
|
|
266
|
-
const btn = $('#lang-toggle')
|
|
267
|
-
if (btn) btn.textContent = window.I18n?.getLang() === 'zh' ? '中文' : 'EN'
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// ── Theme ──────────────────────────────────────────────────────────
|
|
271
|
-
|
|
272
|
-
const html = document.documentElement
|
|
273
|
-
const themeBtn = $('#theme-toggle')
|
|
274
|
-
|
|
275
|
-
function getTheme() {
|
|
276
|
-
return localStorage.getItem('scale-theme') || 'dark'
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function setTheme(theme) {
|
|
280
|
-
html.setAttribute('data-theme', theme)
|
|
281
|
-
localStorage.setItem('scale-theme', theme)
|
|
282
|
-
if (themeBtn) themeBtn.textContent = theme === 'dark' ? '\u263e' : '\u2600'
|
|
283
|
-
window.dispatchEvent(new CustomEvent('themechange', { detail: theme }))
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
setTheme(getTheme())
|
|
287
|
-
|
|
288
|
-
themeBtn.addEventListener('click', () => {
|
|
289
|
-
setTheme(getTheme() === 'dark' ? 'light' : 'dark')
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
// ── Language Toggle ────────────────────────────────────────────────
|
|
293
|
-
|
|
294
|
-
const langBtn = $('#lang-toggle')
|
|
295
|
-
updateLangToggle()
|
|
296
|
-
|
|
297
|
-
langBtn.addEventListener('click', () => {
|
|
298
|
-
const next = window.I18n?.getLang() === 'zh' ? 'en' : 'zh'
|
|
299
|
-
window.I18n?.setLang(next)
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
window.addEventListener('langchange', () => {
|
|
303
|
-
updateLangToggle()
|
|
304
|
-
translateDocument()
|
|
305
|
-
navigate(currentPage, { force: true })
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
// ── Router ─────────────────────────────────────────────────────────
|
|
309
|
-
|
|
310
|
-
const pageKeys = {
|
|
311
|
-
overview: 'overview.title',
|
|
312
|
-
workflow: 'workflow.title',
|
|
313
|
-
topology: 'topology.title',
|
|
314
|
-
monitoring: 'monitoring.title',
|
|
315
|
-
costs: 'costs.title',
|
|
316
|
-
documents: 'documents.title',
|
|
317
|
-
knowledge: 'knowledge.title',
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const pages = {
|
|
321
|
-
overview: { render: () => window.DashboardPages?.overview?.() },
|
|
322
|
-
workflow: { render: () => window.DashboardPages?.workflow?.() },
|
|
323
|
-
topology: { render: () => window.DashboardPages?.topology?.() },
|
|
324
|
-
monitoring: { render: () => window.DashboardPages?.monitoring?.() },
|
|
325
|
-
costs: { render: () => window.DashboardPages?.costs?.() },
|
|
326
|
-
documents: { render: () => window.DashboardPages?.documents?.() },
|
|
327
|
-
knowledge: { render: () => window.DashboardPages?.knowledge?.() },
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
let currentPage = 'overview'
|
|
331
|
-
let chartInstances = []
|
|
332
|
-
|
|
333
|
-
function disposeCharts() {
|
|
334
|
-
chartInstances.forEach(c => {
|
|
335
|
-
try {
|
|
336
|
-
c.dispose()
|
|
337
|
-
} catch (error) {
|
|
338
|
-
observeRecoverableError(error)
|
|
339
|
-
}
|
|
340
|
-
})
|
|
341
|
-
chartInstances = []
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function registerChart(instance) {
|
|
345
|
-
chartInstances.push(instance)
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function navigate(page, options = {}) {
|
|
349
|
-
if (!pages[page]) page = 'overview'
|
|
350
|
-
if (!options.force && currentPage === page && $('#app').children.length > 0) return
|
|
351
|
-
|
|
352
|
-
setPageCleanup(null)
|
|
353
|
-
disposeCharts()
|
|
354
|
-
currentPage = page
|
|
355
|
-
|
|
356
|
-
// Update nav
|
|
357
|
-
$$('.nav-item').forEach(el => {
|
|
358
|
-
el.classList.toggle('active', el.dataset.page === page)
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
// Update title
|
|
362
|
-
const titleEl = $('#page-title')
|
|
363
|
-
if (titleEl) titleEl.textContent = t(pageKeys[page] || page)
|
|
364
|
-
|
|
365
|
-
// Update URL
|
|
366
|
-
history.replaceState(null, '', `#${page}`)
|
|
367
|
-
|
|
368
|
-
// Render page
|
|
369
|
-
const app = $('#app')
|
|
370
|
-
renderLoading(app)
|
|
371
|
-
try {
|
|
372
|
-
const result = pages[page].render?.()
|
|
373
|
-
if (result?.catch) result.catch(e => {
|
|
374
|
-
renderEmptyState(app, errorMessage(e), { icon: '\u26a0' })
|
|
375
|
-
})
|
|
376
|
-
} catch (e) {
|
|
377
|
-
renderEmptyState(app, errorMessage(e), { icon: '\u26a0' })
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Nav click handler
|
|
382
|
-
$$('.nav-item').forEach(el => {
|
|
383
|
-
el.addEventListener('click', () => navigate(el.dataset.page))
|
|
384
|
-
})
|
|
385
|
-
|
|
386
|
-
// Hash routing
|
|
387
|
-
function handleHash() {
|
|
388
|
-
const hash = location.hash.slice(1) || 'overview'
|
|
389
|
-
navigate(hash)
|
|
390
|
-
}
|
|
391
|
-
window.addEventListener('hashchange', handleHash)
|
|
392
|
-
|
|
393
|
-
// ── SSE Connection ─────────────────────────────────────────────────
|
|
394
|
-
|
|
395
|
-
let eventSource = null
|
|
396
|
-
const sseDot = $('#sse-dot')
|
|
397
|
-
const sseLabel = $('#sse-label')
|
|
398
|
-
|
|
399
|
-
function connectSSE() {
|
|
400
|
-
if (eventSource) {
|
|
401
|
-
try {
|
|
402
|
-
eventSource.close()
|
|
403
|
-
} catch (error) {
|
|
404
|
-
observeRecoverableError(error)
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
eventSource = new EventSource('/api/stream')
|
|
409
|
-
|
|
410
|
-
eventSource.addEventListener('init', (e) => {
|
|
411
|
-
sseDot?.classList.add('connected')
|
|
412
|
-
if (sseLabel) sseLabel.textContent = t('sse.live')
|
|
413
|
-
})
|
|
414
|
-
|
|
415
|
-
eventSource.addEventListener('event', (e) => {
|
|
416
|
-
try {
|
|
417
|
-
const data = JSON.parse(e.data)
|
|
418
|
-
window.dispatchEvent(new CustomEvent('scale-event', { detail: data.event }))
|
|
419
|
-
} catch (error) {
|
|
420
|
-
observeRecoverableError(error)
|
|
421
|
-
}
|
|
422
|
-
})
|
|
423
|
-
|
|
424
|
-
eventSource.addEventListener('heartbeat', () => {
|
|
425
|
-
sseDot?.classList.add('connected')
|
|
426
|
-
if (sseLabel) sseLabel.textContent = t('sse.live')
|
|
427
|
-
})
|
|
428
|
-
|
|
429
|
-
eventSource.onerror = () => {
|
|
430
|
-
sseDot?.classList.remove('connected')
|
|
431
|
-
if (sseLabel) sseLabel.textContent = t('sse.reconnecting')
|
|
432
|
-
setTimeout(connectSSE, 5000)
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
connectSSE()
|
|
437
|
-
|
|
438
|
-
// ── Search ─────────────────────────────────────────────────────────
|
|
439
|
-
|
|
440
|
-
const searchBox = $('#global-search')
|
|
441
|
-
searchBox.addEventListener('keydown', (e) => {
|
|
442
|
-
if (e.key === 'Enter') {
|
|
443
|
-
const q = searchBox.value.trim()
|
|
444
|
-
if (q) window.dispatchEvent(new CustomEvent('search', { detail: q }))
|
|
445
|
-
}
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
async function initProjectSwitcher() {
|
|
449
|
-
const actions = $('.header-actions')
|
|
450
|
-
if (!actions) return
|
|
451
|
-
const projects = await fetchJSON('/api/projects')
|
|
452
|
-
if (!Array.isArray(projects) || projects.length === 0) return
|
|
453
|
-
const current = projects.find(project => project.current) || projects[0]
|
|
454
|
-
if (projects.length === 1) {
|
|
455
|
-
actions.insertBefore(el('span', {
|
|
456
|
-
className: 'text-muted text-sm',
|
|
457
|
-
text: current.name,
|
|
458
|
-
title: current.projectDir,
|
|
459
|
-
style: { whiteSpace: 'nowrap', maxWidth: '220px', overflow: 'hidden', textOverflow: 'ellipsis' },
|
|
460
|
-
}), actions.firstChild)
|
|
461
|
-
return
|
|
462
|
-
}
|
|
463
|
-
const select = el('select', {
|
|
464
|
-
className: 'search-box',
|
|
465
|
-
title: 'Project',
|
|
466
|
-
style: { width: '220px' },
|
|
467
|
-
}, projects.map(project => el('option', {
|
|
468
|
-
text: project.name,
|
|
469
|
-
value: project.url || '',
|
|
470
|
-
attrs: { selected: project.current ? true : null },
|
|
471
|
-
})))
|
|
472
|
-
select.addEventListener('change', () => {
|
|
473
|
-
if (select.value) window.location.href = select.value
|
|
474
|
-
})
|
|
475
|
-
actions.insertBefore(select, actions.firstChild)
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
void initProjectSwitcher()
|
|
479
|
-
|
|
480
|
-
// ── Shared State ───────────────────────────────────────────────────
|
|
481
|
-
|
|
482
|
-
window.Dashboard = {
|
|
483
|
-
dom: {
|
|
484
|
-
autoRefreshControl,
|
|
485
|
-
chartContainer,
|
|
486
|
-
copyText,
|
|
487
|
-
dataTable,
|
|
488
|
-
dataNote,
|
|
489
|
-
downloadText,
|
|
490
|
-
el,
|
|
491
|
-
emptyState,
|
|
492
|
-
metricCard,
|
|
493
|
-
panel,
|
|
494
|
-
renderText,
|
|
495
|
-
safeClassToken,
|
|
496
|
-
textBlock,
|
|
497
|
-
},
|
|
498
|
-
fetchJSON,
|
|
499
|
-
formatNumber,
|
|
500
|
-
formatTime,
|
|
501
|
-
relativeTime,
|
|
502
|
-
registerChart,
|
|
503
|
-
renderEmptyState,
|
|
504
|
-
renderLoading,
|
|
505
|
-
getTheme,
|
|
506
|
-
runtimeLabel,
|
|
507
|
-
navigate,
|
|
508
|
-
t,
|
|
509
|
-
$,
|
|
510
|
-
$$,
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// ── Initial Render ─────────────────────────────────────────────────
|
|
514
|
-
// Deferred to after page scripts load (see index.html)
|
|
515
|
-
})()
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DataTable — Reusable sortable data table
|
|
3
|
-
* Usage: Dashboard.components.DataTable({ columns, rows, emptyText })
|
|
4
|
-
*/
|
|
5
|
-
;(() => {
|
|
6
|
-
'use strict'
|
|
7
|
-
|
|
8
|
-
window.Dashboard = window.Dashboard || {}
|
|
9
|
-
window.Dashboard.components = window.Dashboard.components || {}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @param {Object} opts
|
|
13
|
-
* @param {Array<{key: string, label: string, width?: string, render?: Function}>} opts.columns
|
|
14
|
-
* @param {Array<Object>} opts.rows
|
|
15
|
-
* @param {string} opts.emptyText
|
|
16
|
-
* @param {string} opts.cls
|
|
17
|
-
*/
|
|
18
|
-
window.Dashboard.components.DataTable = function DataTable({ columns = [], rows = [], emptyText = 'No data', cls = '' }) {
|
|
19
|
-
if (!rows.length) {
|
|
20
|
-
return `<div class="text-muted" style="padding:24px;text-align:center">${emptyText}</div>`
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const header = columns.map(c =>
|
|
24
|
-
`<th${c.width ? ` style="width:${c.width}"` : ''}>${c.label}</th>`
|
|
25
|
-
).join('')
|
|
26
|
-
|
|
27
|
-
const body = rows.map(row => {
|
|
28
|
-
const cells = columns.map(c => {
|
|
29
|
-
const value = row[c.key]
|
|
30
|
-
const rendered = c.render ? c.render(value, row) : escapeHtml(String(value ?? ''))
|
|
31
|
-
return `<td>${rendered}</td>`
|
|
32
|
-
}).join('')
|
|
33
|
-
return `<tr>${cells}</tr>`
|
|
34
|
-
}).join('')
|
|
35
|
-
|
|
36
|
-
return `
|
|
37
|
-
<div class="table-wrap ${cls}">
|
|
38
|
-
<table class="data-table">
|
|
39
|
-
<thead><tr>${header}</tr></thead>
|
|
40
|
-
<tbody>${body}</tbody>
|
|
41
|
-
</table>
|
|
42
|
-
</div>
|
|
43
|
-
`
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function escapeHtml(str) {
|
|
47
|
-
return str
|
|
48
|
-
.replace(/&/g, '&')
|
|
49
|
-
.replace(/</g, '<')
|
|
50
|
-
.replace(/>/g, '>')
|
|
51
|
-
.replace(/"/g, '"')
|
|
52
|
-
}
|
|
53
|
-
})()
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* EventStream — Real-time event display
|
|
3
|
-
* Usage: Dashboard.components.EventStream(events, opts)
|
|
4
|
-
*/
|
|
5
|
-
;(() => {
|
|
6
|
-
'use strict'
|
|
7
|
-
|
|
8
|
-
window.Dashboard = window.Dashboard || {}
|
|
9
|
-
window.Dashboard.components = window.Dashboard.components || {}
|
|
10
|
-
|
|
11
|
-
const { relativeTime, t } = window.Dashboard
|
|
12
|
-
|
|
13
|
-
const EVENT_ICONS = {
|
|
14
|
-
'tool.completed': '✓',
|
|
15
|
-
'tool.failed': '✗',
|
|
16
|
-
'review.required': '🔍',
|
|
17
|
-
'review.passed': '✓',
|
|
18
|
-
'review.failed': '✗',
|
|
19
|
-
'hook.deployed': '🔗',
|
|
20
|
-
'hook.generated': '⚙',
|
|
21
|
-
'behavior.brute_retry': '⚠',
|
|
22
|
-
'behavior.premature_done': '⚠',
|
|
23
|
-
'behavior.blame_shift': '⚠',
|
|
24
|
-
'evolution.cycle_completed': '🔄',
|
|
25
|
-
'task.transition': '→',
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const EVENT_COLORS = {
|
|
29
|
-
'tool.completed': 'accent',
|
|
30
|
-
'tool.failed': 'danger',
|
|
31
|
-
'review.passed': 'success',
|
|
32
|
-
'review.failed': 'danger',
|
|
33
|
-
'hook.deployed': 'info',
|
|
34
|
-
'behavior.brute_retry': 'warning',
|
|
35
|
-
'behavior.premature_done': 'warning',
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @param {Array} events
|
|
40
|
-
* @param {Object} opts
|
|
41
|
-
* @param {number} opts.limit - Max events to show (default 20)
|
|
42
|
-
* @param {boolean} opts.showTime - Show relative time (default true)
|
|
43
|
-
*/
|
|
44
|
-
window.Dashboard.components.EventStream = function EventStream(events = [], opts = {}) {
|
|
45
|
-
const limit = opts.limit ?? 20
|
|
46
|
-
const showTime = opts.showTime !== false
|
|
47
|
-
const sliced = events.slice(0, limit)
|
|
48
|
-
|
|
49
|
-
if (!sliced.length) {
|
|
50
|
-
return `<div class="text-muted" style="padding:16px;text-align:center">${t('common.noEvents') || 'No events'}</div>`
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return `<div class="event-stream">${sliced.map(ev => {
|
|
54
|
-
const icon = EVENT_ICONS[ev.type] || '•'
|
|
55
|
-
const color = EVENT_COLORS[ev.type] || 'text-2'
|
|
56
|
-
const typeLabel = ev.type.split('.').pop()
|
|
57
|
-
return `
|
|
58
|
-
<div class="event-item">
|
|
59
|
-
<span class="event-icon ${color}">${icon}</span>
|
|
60
|
-
<span class="event-type">${typeLabel}</span>
|
|
61
|
-
${showTime ? `<span class="event-time">${relativeTime(ev.timestamp)}</span>` : ''}
|
|
62
|
-
</div>
|
|
63
|
-
`
|
|
64
|
-
}).join('')}</div>`
|
|
65
|
-
}
|
|
66
|
-
})()
|