@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.
Files changed (50) hide show
  1. package/README.en.md +2 -2
  2. package/README.md +2 -2
  3. package/dist/api/http.js +3 -1
  4. package/dist/api/http.js.map +1 -1
  5. package/dist/cli/cortexCommands.d.ts +16 -0
  6. package/dist/cli/cortexCommands.js +47 -4
  7. package/dist/cli/cortexCommands.js.map +1 -1
  8. package/dist/cortex/InstinctStore.d.ts +13 -1
  9. package/dist/cortex/InstinctStore.js +90 -11
  10. package/dist/cortex/InstinctStore.js.map +1 -1
  11. package/dist/cortex/SessionInjector.js +39 -2
  12. package/dist/cortex/SessionInjector.js.map +1 -1
  13. package/dist/dashboard/DashboardServer.d.ts +158 -0
  14. package/dist/dashboard/DashboardServer.js +753 -13
  15. package/dist/dashboard/DashboardServer.js.map +1 -1
  16. package/dist/dashboard/spa/assets/index-VYBCLBje.js +11 -0
  17. package/dist/dashboard/spa/assets/index-VhwY_ac1.css +1 -0
  18. package/dist/dashboard/spa/assets/naive-ui-BQy2AJkt.js +3340 -0
  19. package/dist/dashboard/spa/assets/vendor-BPU6aOYA.js +3 -0
  20. package/dist/dashboard/spa/assets/vue-CQQMb5Wi.js +17 -0
  21. package/dist/dashboard/spa/index.html +15 -462
  22. package/dist/memory/MemoryFabric.d.ts +13 -1
  23. package/dist/memory/MemoryFabric.js +60 -0
  24. package/dist/memory/MemoryFabric.js.map +1 -1
  25. package/dist/version.d.ts +1 -1
  26. package/dist/version.js +1 -1
  27. package/docs/workflow/ASSESSMENT_INDEX.md +326 -0
  28. package/docs/workflow/COMPARATIVE_ANALYSIS.md +422 -0
  29. package/docs/workflow/EXECUTIVE_SUMMARY.md +310 -0
  30. package/docs/workflow/IMPROVEMENT_CHECKLIST.md +518 -0
  31. package/docs/workflow/IMPROVEMENT_ROADMAP.md +707 -0
  32. package/docs/workflow/README.md +8 -0
  33. package/package.json +6 -2
  34. package/dist/dashboard/spa/app.js +0 -515
  35. package/dist/dashboard/spa/components/DataTable.js +0 -53
  36. package/dist/dashboard/spa/components/EventStream.js +0 -66
  37. package/dist/dashboard/spa/components/LoadingState.js +0 -39
  38. package/dist/dashboard/spa/components/MetricCard.js +0 -30
  39. package/dist/dashboard/spa/components/Panel.js +0 -27
  40. package/dist/dashboard/spa/components/StatusBadge.js +0 -51
  41. package/dist/dashboard/spa/i18n.js +0 -767
  42. package/dist/dashboard/spa/pages/costs.js +0 -522
  43. package/dist/dashboard/spa/pages/documents.js +0 -540
  44. package/dist/dashboard/spa/pages/knowledge.js +0 -457
  45. package/dist/dashboard/spa/pages/monitoring.js +0 -361
  46. package/dist/dashboard/spa/pages/overview.js +0 -301
  47. package/dist/dashboard/spa/pages/topology-renderers.js +0 -251
  48. package/dist/dashboard/spa/pages/topology.js +0 -370
  49. package/dist/dashboard/spa/pages/workflow-renderers.js +0 -239
  50. package/dist/dashboard/spa/pages/workflow.js +0 -217
@@ -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.1",
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/copy-dashboard-spa.mjs",
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, '&amp;')
49
- .replace(/</g, '&lt;')
50
- .replace(/>/g, '&gt;')
51
- .replace(/"/g, '&quot;')
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': '&#10003;',
15
- 'tool.failed': '&#10007;',
16
- 'review.required': '&#128269;',
17
- 'review.passed': '&#10003;',
18
- 'review.failed': '&#10007;',
19
- 'hook.deployed': '&#128279;',
20
- 'hook.generated': '&#9881;',
21
- 'behavior.brute_retry': '&#9888;',
22
- 'behavior.premature_done': '&#9888;',
23
- 'behavior.blame_shift': '&#9888;',
24
- 'evolution.cycle_completed': '&#128260;',
25
- 'task.transition': '&#8594;',
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] || '&#8226;'
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
- })()