@hongmaple0820/scale-engine 0.49.0 → 0.50.1

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 (190) hide show
  1. package/README.en.md +2 -2
  2. package/README.md +2 -2
  3. package/dist/api/DashboardHttpConfig.d.ts +28 -0
  4. package/dist/api/DashboardHttpConfig.js +110 -0
  5. package/dist/api/DashboardHttpConfig.js.map +1 -0
  6. package/dist/api/cli.js +102 -11
  7. package/dist/api/cli.js.map +1 -1
  8. package/dist/api/http.d.ts +1 -0
  9. package/dist/api/http.js +50 -0
  10. package/dist/api/http.js.map +1 -0
  11. package/dist/artifact/types.d.ts +5 -0
  12. package/dist/artifact/types.js.map +1 -1
  13. package/dist/bootstrap/DependencyBootstrap.d.ts +1 -0
  14. package/dist/bootstrap/DependencyBootstrap.js +14 -3
  15. package/dist/bootstrap/DependencyBootstrap.js.map +1 -1
  16. package/dist/cli/cortexApplyCommand.d.ts +26 -0
  17. package/dist/cli/cortexApplyCommand.js +74 -0
  18. package/dist/cli/cortexApplyCommand.js.map +1 -0
  19. package/dist/cli/cortexCandidateCommands.d.ts +42 -0
  20. package/dist/cli/cortexCandidateCommands.js +119 -0
  21. package/dist/cli/cortexCandidateCommands.js.map +1 -0
  22. package/dist/cli/cortexCommands.d.ts +15 -0
  23. package/dist/cli/cortexCommands.js +57 -15
  24. package/dist/cli/cortexCommands.js.map +1 -1
  25. package/dist/cli/engineBootstrap.d.ts +1 -1
  26. package/dist/cli/engineBootstrap.js +2 -0
  27. package/dist/cli/engineBootstrap.js.map +1 -1
  28. package/dist/cli/evalCommands.js +1 -0
  29. package/dist/cli/evalCommands.js.map +1 -1
  30. package/dist/cli/phaseCommands.d.ts +28 -0
  31. package/dist/cli/phaseCommands.js +148 -9
  32. package/dist/cli/phaseCommands.js.map +1 -1
  33. package/dist/cli/runtimeSkillCommands.js +12 -2
  34. package/dist/cli/runtimeSkillCommands.js.map +1 -1
  35. package/dist/cli/shieldCommands.d.ts +1 -0
  36. package/dist/cli/shieldCommands.js +20 -7
  37. package/dist/cli/shieldCommands.js.map +1 -1
  38. package/dist/cli/workflowEvidenceCommands.d.ts +120 -0
  39. package/dist/cli/workflowEvidenceCommands.js +228 -2
  40. package/dist/cli/workflowEvidenceCommands.js.map +1 -1
  41. package/dist/cortex/AutoFixEventObservations.d.ts +11 -0
  42. package/dist/cortex/AutoFixEventObservations.js +72 -0
  43. package/dist/cortex/AutoFixEventObservations.js.map +1 -0
  44. package/dist/cortex/GateEvidenceObservations.d.ts +22 -0
  45. package/dist/cortex/GateEvidenceObservations.js +179 -0
  46. package/dist/cortex/GateEvidenceObservations.js.map +1 -0
  47. package/dist/cortex/GovernanceMetrics.d.ts +2 -0
  48. package/dist/cortex/GovernanceMetrics.js +112 -22
  49. package/dist/cortex/GovernanceMetrics.js.map +1 -1
  50. package/dist/cortex/InstinctApplicationRecorder.d.ts +28 -0
  51. package/dist/cortex/InstinctApplicationRecorder.js +145 -0
  52. package/dist/cortex/InstinctApplicationRecorder.js.map +1 -0
  53. package/dist/cortex/InstinctCandidateAudit.d.ts +3 -0
  54. package/dist/cortex/InstinctCandidateAudit.js +39 -0
  55. package/dist/cortex/InstinctCandidateAudit.js.map +1 -0
  56. package/dist/cortex/InstinctCandidateReview.d.ts +32 -0
  57. package/dist/cortex/InstinctCandidateReview.js +125 -0
  58. package/dist/cortex/InstinctCandidateReview.js.map +1 -0
  59. package/dist/cortex/InstinctExtractor.d.ts +1 -0
  60. package/dist/cortex/InstinctExtractor.js +24 -17
  61. package/dist/cortex/InstinctExtractor.js.map +1 -1
  62. package/dist/cortex/InstinctRuntimeEvidence.d.ts +14 -0
  63. package/dist/cortex/InstinctRuntimeEvidence.js +120 -0
  64. package/dist/cortex/InstinctRuntimeEvidence.js.map +1 -0
  65. package/dist/cortex/InstinctStore.d.ts +18 -3
  66. package/dist/cortex/InstinctStore.js +30 -9
  67. package/dist/cortex/InstinctStore.js.map +1 -1
  68. package/dist/cortex/SessionInjector.d.ts +1 -0
  69. package/dist/cortex/SessionInjector.js +15 -2
  70. package/dist/cortex/SessionInjector.js.map +1 -1
  71. package/dist/dashboard/DashboardServer.d.ts +79 -0
  72. package/dist/dashboard/DashboardServer.js +330 -6
  73. package/dist/dashboard/DashboardServer.js.map +1 -1
  74. package/dist/dashboard/spa/app.js +515 -0
  75. package/dist/dashboard/spa/components/DataTable.js +53 -0
  76. package/dist/dashboard/spa/components/EventStream.js +66 -0
  77. package/dist/dashboard/spa/components/LoadingState.js +39 -0
  78. package/dist/dashboard/spa/components/MetricCard.js +30 -0
  79. package/dist/dashboard/spa/components/Panel.js +27 -0
  80. package/dist/dashboard/spa/components/StatusBadge.js +51 -0
  81. package/dist/dashboard/spa/i18n.js +767 -0
  82. package/dist/dashboard/spa/index.html +463 -0
  83. package/dist/dashboard/spa/pages/costs.js +522 -0
  84. package/dist/dashboard/spa/pages/documents.js +540 -0
  85. package/dist/dashboard/spa/pages/knowledge.js +457 -0
  86. package/dist/dashboard/spa/pages/monitoring.js +361 -0
  87. package/dist/dashboard/spa/pages/overview.js +301 -0
  88. package/dist/dashboard/spa/pages/topology-renderers.js +251 -0
  89. package/dist/dashboard/spa/pages/topology.js +370 -0
  90. package/dist/dashboard/spa/pages/workflow-renderers.js +239 -0
  91. package/dist/dashboard/spa/pages/workflow.js +217 -0
  92. package/dist/env/EnvironmentDoctor.js +12 -7
  93. package/dist/env/EnvironmentDoctor.js.map +1 -1
  94. package/dist/eval/WorkflowEval.d.ts +9 -0
  95. package/dist/eval/WorkflowEval.js +348 -2
  96. package/dist/eval/WorkflowEval.js.map +1 -1
  97. package/dist/memory/MemoryBrain.d.ts +13 -0
  98. package/dist/memory/MemoryBrain.js +47 -0
  99. package/dist/memory/MemoryBrain.js.map +1 -1
  100. package/dist/memory/MemoryFabric.d.ts +1 -0
  101. package/dist/memory/MemoryFabric.js +12 -8
  102. package/dist/memory/MemoryFabric.js.map +1 -1
  103. package/dist/memory/MemoryLearning.d.ts +1 -0
  104. package/dist/memory/MemoryLearning.js +6 -3
  105. package/dist/memory/MemoryLearning.js.map +1 -1
  106. package/dist/memory/MemoryProviders.d.ts +8 -1
  107. package/dist/memory/MemoryProviders.js +143 -29
  108. package/dist/memory/MemoryProviders.js.map +1 -1
  109. package/dist/runtime/AiOsRuntime.d.ts +14 -1
  110. package/dist/runtime/AiOsRuntime.js +59 -3
  111. package/dist/runtime/AiOsRuntime.js.map +1 -1
  112. package/dist/runtime/RuntimeDoctor.js +3 -1
  113. package/dist/runtime/RuntimeDoctor.js.map +1 -1
  114. package/dist/runtime/RuntimeEvidenceLedger.d.ts +6 -0
  115. package/dist/runtime/RuntimeEvidenceLedger.js +52 -1
  116. package/dist/runtime/RuntimeEvidenceLedger.js.map +1 -1
  117. package/dist/runtime/SessionLedger.d.ts +2 -0
  118. package/dist/runtime/SessionLedger.js +4 -0
  119. package/dist/runtime/SessionLedger.js.map +1 -1
  120. package/dist/setup/SetupVerification.js +53 -5
  121. package/dist/setup/SetupVerification.js.map +1 -1
  122. package/dist/shield/PolicyCompiler.js +73 -12
  123. package/dist/shield/PolicyCompiler.js.map +1 -1
  124. package/dist/shield/ProtectedPaths.js +4 -2
  125. package/dist/shield/ProtectedPaths.js.map +1 -1
  126. package/dist/skills/SkillCatalog.d.ts +2 -0
  127. package/dist/skills/SkillCatalog.js +8 -0
  128. package/dist/skills/SkillCatalog.js.map +1 -1
  129. package/dist/skills/SkillDoctor.d.ts +19 -2
  130. package/dist/skills/SkillDoctor.js +163 -13
  131. package/dist/skills/SkillDoctor.js.map +1 -1
  132. package/dist/tools/SafeCommandRunner.d.ts +1 -0
  133. package/dist/tools/SafeCommandRunner.js +1 -0
  134. package/dist/tools/SafeCommandRunner.js.map +1 -1
  135. package/dist/tools/ToolCapabilityRegistry.js +25 -3
  136. package/dist/tools/ToolCapabilityRegistry.js.map +1 -1
  137. package/dist/tools/ToolOrchestrator.js +21 -0
  138. package/dist/tools/ToolOrchestrator.js.map +1 -1
  139. package/dist/version.d.ts +1 -1
  140. package/dist/version.js +1 -1
  141. package/dist/workflow/AgentLoopReadiness.d.ts +103 -0
  142. package/dist/workflow/AgentLoopReadiness.js +371 -0
  143. package/dist/workflow/AgentLoopReadiness.js.map +1 -0
  144. package/dist/workflow/EcosystemReadinessGate.d.ts +46 -0
  145. package/dist/workflow/EcosystemReadinessGate.js +126 -0
  146. package/dist/workflow/EcosystemReadinessGate.js.map +1 -0
  147. package/dist/workflow/EngineeringStandards.js +48 -3
  148. package/dist/workflow/EngineeringStandards.js.map +1 -1
  149. package/dist/workflow/GateCatalog.js +9 -0
  150. package/dist/workflow/GateCatalog.js.map +1 -1
  151. package/dist/workflow/GovernanceTemplatePacks.js +2 -26
  152. package/dist/workflow/GovernanceTemplatePacks.js.map +1 -1
  153. package/dist/workflow/GovernanceTemplates.js +8 -1
  154. package/dist/workflow/GovernanceTemplates.js.map +1 -1
  155. package/dist/workflow/ReleaseDeploymentLedger.d.ts +63 -0
  156. package/dist/workflow/ReleaseDeploymentLedger.js +154 -0
  157. package/dist/workflow/ReleaseDeploymentLedger.js.map +1 -0
  158. package/dist/workflow/ReviewAnalyzer.js +50 -3
  159. package/dist/workflow/ReviewAnalyzer.js.map +1 -1
  160. package/dist/workflow/SessionPreamble.d.ts +7 -0
  161. package/dist/workflow/SessionPreamble.js +48 -9
  162. package/dist/workflow/SessionPreamble.js.map +1 -1
  163. package/dist/workflow/VerificationCommands.d.ts +1 -0
  164. package/dist/workflow/VerificationCommands.js.map +1 -1
  165. package/dist/workflow/VerificationProfile.d.ts +5 -0
  166. package/dist/workflow/VerificationProfile.js +26 -0
  167. package/dist/workflow/VerificationProfile.js.map +1 -1
  168. package/dist/workflow/VerificationSchema.d.ts +3 -0
  169. package/dist/workflow/VerificationSchema.js +6 -0
  170. package/dist/workflow/VerificationSchema.js.map +1 -1
  171. package/dist/workflow/WorkflowEffectiveness.d.ts +97 -0
  172. package/dist/workflow/WorkflowEffectiveness.js +302 -0
  173. package/dist/workflow/WorkflowEffectiveness.js.map +1 -0
  174. package/dist/workflow/WorkflowEffectivenessRenderer.d.ts +2 -0
  175. package/dist/workflow/WorkflowEffectivenessRenderer.js +67 -0
  176. package/dist/workflow/WorkflowEffectivenessRenderer.js.map +1 -0
  177. package/dist/workflow/WorkflowEffectivenessScoring.d.ts +6 -0
  178. package/dist/workflow/WorkflowEffectivenessScoring.js +243 -0
  179. package/dist/workflow/WorkflowEffectivenessScoring.js.map +1 -0
  180. package/dist/workflow/gates/GateSystem.d.ts +16 -0
  181. package/dist/workflow/gates/GateSystem.js +208 -41
  182. package/dist/workflow/gates/GateSystem.js.map +1 -1
  183. package/dist/workflow/gates/MetaGovernanceGates.js +269 -8
  184. package/dist/workflow/gates/MetaGovernanceGates.js.map +1 -1
  185. package/docs/reference/cli.md +2 -1
  186. package/docs/start/agent-governance-demo.md +1 -1
  187. package/docs/workflow/README.md +1 -1
  188. package/docs/workflow/templates/github-actions-scale-preflight.yml +4 -1
  189. package/package.json +6 -3
  190. package/scripts/workflow/run-vitest.mjs +123 -0
@@ -0,0 +1,515 @@
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
+ })()
@@ -0,0 +1,53 @@
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
+ })()
@@ -0,0 +1,66 @@
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
+ })()
@@ -0,0 +1,39 @@
1
+ /**
2
+ * LoadingState — Loading and empty state components
3
+ * Usage: Dashboard.components.LoadingState(message)
4
+ * Dashboard.components.EmptyState({ icon, title, description })
5
+ */
6
+ ;(() => {
7
+ 'use strict'
8
+
9
+ window.Dashboard = window.Dashboard || {}
10
+ window.Dashboard.components = window.Dashboard.components || {}
11
+
12
+ window.Dashboard.components.LoadingState = function LoadingState(message = 'Loading...') {
13
+ return `
14
+ <div class="loading-state">
15
+ <div class="loading-spinner"></div>
16
+ <div class="text-muted">${message}</div>
17
+ </div>
18
+ `
19
+ }
20
+
21
+ window.Dashboard.components.EmptyState = function EmptyState({ icon = '&#128196;', title = 'No data', description = '' } = {}) {
22
+ return `
23
+ <div class="empty-state">
24
+ <div class="empty-icon">${icon}</div>
25
+ <div class="empty-title">${title}</div>
26
+ ${description ? `<div class="empty-desc text-muted">${description}</div>` : ''}
27
+ </div>
28
+ `
29
+ }
30
+
31
+ window.Dashboard.components.ErrorState = function ErrorState(message = 'Something went wrong') {
32
+ return `
33
+ <div class="empty-state">
34
+ <div class="empty-icon" style="color:var(--danger)">&#9888;</div>
35
+ <div class="empty-title">${message}</div>
36
+ </div>
37
+ `
38
+ }
39
+ })()
@@ -0,0 +1,30 @@
1
+ /**
2
+ * MetricCard — Reusable metric display card
3
+ * Usage: Dashboard.components.MetricCard({ label, value, cls, icon })
4
+ */
5
+ ;(() => {
6
+ 'use strict'
7
+
8
+ window.Dashboard = window.Dashboard || {}
9
+ window.Dashboard.components = window.Dashboard.components || {}
10
+
11
+ window.Dashboard.components.MetricCard = function MetricCard({ label, value, cls = '', icon = '' }) {
12
+ return `
13
+ <div class="metric-card">
14
+ ${icon ? `<div class="metric-icon">${icon}</div>` : ''}
15
+ <div class="metric-label">${label}</div>
16
+ <div class="metric-value ${cls}">${value}</div>
17
+ </div>
18
+ `
19
+ }
20
+
21
+ /**
22
+ * MetricCardRow — Row of metric cards
23
+ * Usage: Dashboard.components.MetricRow(cards)
24
+ */
25
+ window.Dashboard.components.MetricRow = function MetricRow(cards) {
26
+ return `<div class="metrics-row">${cards.map(c =>
27
+ window.Dashboard.components.MetricCard(c)
28
+ ).join('')}</div>`
29
+ }
30
+ })()