@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
@@ -1,239 +0,0 @@
1
- /**
2
- * Workflow page renderers. Kept separate from workflow data flow so the page stays small enough for standards gates.
3
- */
4
- ;(() => {
5
- 'use strict'
6
-
7
- const { relativeTime, registerChart, getTheme, runtimeLabel, t, $, dom } = window.Dashboard
8
- const { chartContainer, el, emptyState, safeClassToken } = dom
9
- const STATUS_COLORS = { DRAFT: '#666', REVIEWING: '#ffaa00', FROZEN: '#5588ff', COMPLETED: '#00dc82', BLOCKED: '#ff4444', IN_PROGRESS: '#ffaa00', DONE: '#00dc82', PROPOSED: '#5588ff', APPROVED: '#00dc82', REJECTED: '#ff4444' }
10
-
11
- function renderCards(container, artifacts, ctx) {
12
- if (artifacts.length === 0) {
13
- container.replaceChildren(emptyState(t('workflow.noArtifactMatch'), '\uD83D\uDCC4'))
14
- return
15
- }
16
- container.replaceChildren(el('div', { className: 'artifact-grid' }, artifacts.map(artifact => artifactCard(artifact))))
17
- ctx.wireActionButtons(container)
18
- }
19
-
20
- function artifactCard(artifact) {
21
- const gates = artifact.gates ?? []
22
- const children = [
23
- el('div', { className: 'artifact-card-header' }, [
24
- el('span', { className: 'artifact-card-title', text: artifact.title ?? artifact.id ?? '' }),
25
- statusBadge(artifact.status),
26
- ]),
27
- el('div', { className: 'artifact-card-meta', text: `${artifact.type ?? '-'} \u00b7 v${artifact.version ?? '?'} \u00b7 ${relativeTime(artifact.createdAt)}` }),
28
- ]
29
- if (gates.length > 0) children.push(gateProgressBlock(gates))
30
- if (artifact.children?.length) children.push(el('div', { text: t('workflow.childArtifacts', { count: artifact.children.length }), style: { fontSize: '11px', color: 'var(--text-2)', marginTop: '6px' } }))
31
- children.push(actionButtons(artifact))
32
- return el('div', { className: 'artifact-card', dataset: { id: artifact.id ?? '' } }, children)
33
- }
34
-
35
- function renderTable(container, artifacts, ctx) {
36
- if (artifacts.length === 0) {
37
- container.replaceChildren(emptyState(t('workflow.noArtifactMatch')))
38
- return
39
- }
40
- const headers = [
41
- sortableHeader('title', t('workflow.colTitle'), ctx),
42
- sortableHeader('type', t('workflow.colType'), ctx),
43
- sortableHeader('status', t('workflow.colStatus'), ctx),
44
- sortableHeader('version', t('workflow.colVersion'), ctx),
45
- el('th', { text: t('workflow.colGates') }),
46
- el('th', { text: t('workflow.colCreated') }),
47
- el('th', { text: t('workflow.colActions') }),
48
- ]
49
- container.replaceChildren(el('table', { className: 'data-table' }, [
50
- el('thead', {}, [el('tr', {}, headers)]),
51
- el('tbody', {}, artifacts.map(tableRow)),
52
- ]))
53
- ;[...container.querySelectorAll('th[data-sort]')].forEach(header => {
54
- header.addEventListener('click', () => ctx.onSort(header.dataset.sort))
55
- })
56
- ctx.wireActionButtons(container)
57
- }
58
-
59
- function tableRow(artifact) {
60
- const gates = artifact.gates ?? []
61
- const passed = gates.filter(gate => gate.passed).length
62
- const gateCell = gates.length > 0
63
- ? el('span', { text: `${passed}/${gates.length}`, style: { color: passed === gates.length ? '#00dc82' : '#ffaa00' } })
64
- : el('span', { className: 'text-muted', text: '-' })
65
- return el('tr', {}, [
66
- el('td', { text: artifact.title ?? artifact.id ?? '', style: { fontWeight: '500' } }),
67
- el('td', { className: 'text-muted', text: artifact.type ?? '-' }),
68
- el('td', {}, [statusBadge(artifact.status)]),
69
- el('td', { className: 'text-muted', text: `v${artifact.version ?? '?'}` }),
70
- el('td', {}, [gateCell]),
71
- el('td', { className: 'text-muted text-sm', text: relativeTime(artifact.createdAt) }),
72
- el('td', {}, [actionButtons(artifact, true)]),
73
- ])
74
- }
75
-
76
- function renderDependencyGraph(container, artifacts) {
77
- if (artifacts.length === 0) {
78
- container.replaceChildren(emptyState(t('workflow.noArtifacts'), '\uD83D\uDCC4'))
79
- return
80
- }
81
- const graph = chartContainer(t('workflow.artifactDependencyGraph'), 'wf-dep-graph')
82
- graph.querySelector('.chart-header')?.appendChild(el('span', { className: 'text-muted text-sm', text: t('workflow.artifactCount', { count: artifacts.length }) }))
83
- const graphArea = graph.querySelector('#wf-dep-graph')
84
- if (graphArea) graphArea.style.height = '500px'
85
- container.replaceChildren(graph)
86
-
87
- const graphNode = $('#wf-dep-graph')
88
- if (!graphNode) return
89
- const chart = echarts.init(graphNode, getTheme() === 'dark' ? 'dark' : null)
90
- registerChart(chart)
91
-
92
- const nodes = artifacts.map(artifact => ({
93
- id: artifact.id,
94
- name: truncate(artifact.title ?? artifact.id ?? '', 25),
95
- symbolSize: 12 + (artifact.children?.length ?? 0) * 4,
96
- itemStyle: { color: STATUS_COLORS[artifact.status] || '#888' },
97
- category: artifact.type,
98
- }))
99
- const links = []
100
- for (const artifact of artifacts) for (const child of artifact.children ?? []) links.push({ source: artifact.id, target: child.id })
101
- const categories = [...new Set(artifacts.map(artifact => artifact.type).filter(Boolean))].map(type => ({ name: type }))
102
-
103
- chart.setOption({
104
- tooltip: { renderMode: 'richText', formatter: params => nodeTooltip(params, artifacts) },
105
- legend: { data: categories.map(category => category.name), textStyle: { color: '#a1a1a1', fontSize: 11 }, bottom: 0, type: 'scroll' },
106
- series: [{ type: 'graph', layout: 'force', roam: true, draggable: true, force: { repulsion: 200, gravity: 0.1, edgeLength: 80 }, label: { show: true, fontSize: 10, color: '#a1a1a1' }, data: nodes, links, categories, lineStyle: { color: '#444', curveness: 0.1 }, emphasis: { focus: 'adjacency', lineStyle: { width: 3 } } }],
107
- })
108
- }
109
-
110
- function renderGateAnalysis(container, artifacts) {
111
- if (artifacts.length === 0) {
112
- container.replaceChildren(emptyState(t('workflow.noArtifacts'), '\uD83D\uDCC4'))
113
- return
114
- }
115
- container.replaceChildren(
116
- el('div', { className: 'grid-2 mb-24' }, [chartContainer(t('workflow.gatePassRate'), 'wf-radar'), chartContainer(t('workflow.typeDistribution'), 'wf-type-chart')]),
117
- el('div', { className: 'grid-2' }, [chartContainer(t('workflow.statusDistribution'), 'wf-status-chart'), chartContainer(t('workflow.gateFailuresByName'), 'wf-gate-bar')])
118
- )
119
- const gateStats = {}
120
- for (const artifact of artifacts) for (const gate of artifact.gates ?? []) {
121
- if (!gateStats[gate.name]) gateStats[gate.name] = { passed: 0, total: 0 }
122
- gateStats[gate.name].total++
123
- if (gate.passed) gateStats[gate.name].passed++
124
- }
125
- renderGateRadar(gateStats)
126
- renderTypeDistribution(artifacts)
127
- renderStatusDistribution(artifacts)
128
- renderGateFailures(gateStats)
129
- }
130
-
131
- function renderGateRadar(gateStats) {
132
- const node = $('#wf-radar')
133
- const gateNames = Object.keys(gateStats).slice(0, 10)
134
- if (!node) return
135
- if (gateNames.length === 0) return node.replaceChildren(emptyState(t('common.noData')))
136
- const radar = echarts.init(node, getTheme() === 'dark' ? 'dark' : null)
137
- registerChart(radar)
138
- radar.setOption({
139
- tooltip: {},
140
- radar: { indicator: gateNames.map(gate => ({ name: gate, max: gateStats[gate].total })), axisName: { color: '#a1a1a1', fontSize: 10 }, splitArea: { areaStyle: { color: ['rgba(0,220,130,0.02)', 'rgba(0,220,130,0.05)'] } } },
141
- series: [{ type: 'radar', data: [
142
- { value: gateNames.map(gate => gateStats[gate].passed), name: t('monitoring.passed'), areaStyle: { color: 'rgba(0,220,130,0.2)' }, lineStyle: { color: '#00dc82' }, itemStyle: { color: '#00dc82' } },
143
- { value: gateNames.map(gate => gateStats[gate].total - gateStats[gate].passed), name: t('monitoring.failed'), areaStyle: { color: 'rgba(255,68,68,0.2)' }, lineStyle: { color: '#ff4444' }, itemStyle: { color: '#ff4444' } },
144
- ] }],
145
- })
146
- }
147
-
148
- function renderTypeDistribution(artifacts) {
149
- const node = $('#wf-type-chart')
150
- if (!node) return
151
- const counts = {}
152
- for (const artifact of artifacts) counts[artifact.type] = (counts[artifact.type] ?? 0) + 1
153
- const entries = Object.entries(counts)
154
- if (entries.length === 0) return node.replaceChildren(emptyState(t('common.noData')))
155
- const chart = echarts.init(node, getTheme() === 'dark' ? 'dark' : null)
156
- registerChart(chart)
157
- chart.setOption({ tooltip: { trigger: 'item' }, series: [{ type: 'pie', radius: ['35%', '65%'], label: { color: '#a1a1a1', fontSize: 11 }, data: entries.map(([type, count]) => ({ name: type, value: count })) }] })
158
- }
159
-
160
- function renderStatusDistribution(artifacts) {
161
- const node = $('#wf-status-chart')
162
- if (!node) return
163
- const counts = {}
164
- for (const artifact of artifacts) counts[artifact.status] = (counts[artifact.status] ?? 0) + 1
165
- const entries = Object.entries(counts)
166
- if (entries.length === 0) return node.replaceChildren(emptyState(t('common.noData')))
167
- const chart = echarts.init(node, getTheme() === 'dark' ? 'dark' : null)
168
- registerChart(chart)
169
- chart.setOption({ tooltip: { trigger: 'item' }, series: [{ type: 'pie', radius: ['35%', '65%'], label: { color: '#a1a1a1', fontSize: 11 }, data: entries.map(([status, count]) => ({ name: runtimeLabel('status', status), value: count, itemStyle: { color: STATUS_COLORS[status] || '#888' } })) }] })
170
- }
171
-
172
- function renderGateFailures(gateStats) {
173
- const node = $('#wf-gate-bar')
174
- if (!node) return
175
- const failures = Object.entries(gateStats).map(([name, stats]) => ({ name, failed: stats.total - stats.passed })).filter(gate => gate.failed > 0).sort((left, right) => right.failed - left.failed).slice(0, 10)
176
- if (failures.length === 0) return node.replaceChildren(emptyState(t('common.noData')))
177
- const chart = echarts.init(node, getTheme() === 'dark' ? 'dark' : null)
178
- registerChart(chart)
179
- chart.setOption({
180
- tooltip: { trigger: 'axis' },
181
- grid: { left: 120, right: 20, top: 10, bottom: 30 },
182
- xAxis: { type: 'value', axisLabel: { color: '#a1a1a1' }, splitLine: { lineStyle: { color: '#2a2a2a' } } },
183
- yAxis: { type: 'category', data: failures.map(gate => gate.name), axisLabel: { color: '#a1a1a1', fontSize: 11 } },
184
- series: [{ type: 'bar', data: failures.map(gate => ({ value: gate.failed, itemStyle: { color: '#ff4444', borderRadius: [0, 4, 4, 0] } })), barWidth: 18 }],
185
- })
186
- }
187
-
188
- function actionButtons(artifact, compact = false) {
189
- const style = { marginTop: compact ? '0' : '10px', display: 'flex', gap: compact ? '4px' : '6px', flexWrap: 'wrap' }
190
- return el('div', { style }, (artifact.availableActions ?? []).map(action => el('button', {
191
- className: 'topo-btn wf-action',
192
- text: formatAction(action),
193
- dataset: { id: artifact.id ?? '', action },
194
- style: compact ? { fontSize: '11px', padding: '3px 8px' } : {},
195
- })))
196
- }
197
-
198
- function gateProgressBlock(gates) {
199
- const passed = gates.filter(gate => gate.passed).length
200
- const progress = gates.length > 0 ? Math.round((passed / gates.length) * 100) : 0
201
- return el('div', { style: { margin: '10px 0' } }, [
202
- el('div', { style: { display: 'flex', justifyContent: 'space-between', fontSize: '11px', color: 'var(--text-2)', marginBottom: '4px' } }, [el('span', { text: t('workflow.gates') }), el('span', { text: `${passed}/${gates.length}` })]),
203
- el('div', { style: { height: '4px', background: 'var(--bg-3)', borderRadius: '2px', overflow: 'hidden' } }, [el('div', { style: { height: '100%', width: `${progress}%`, background: gateProgressColor(progress), borderRadius: '2px', transition: 'width 0.3s' } })]),
204
- el('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px', marginTop: '6px' } }, gates.map(gate => el('span', { className: `gate-pill ${gate.passed ? 'passed' : 'failed'}`, text: gate.name, title: gate.name }))),
205
- ])
206
- }
207
-
208
- function sortableHeader(column, label, ctx) {
209
- const active = ctx.sortCol() === column
210
- const icon = active ? (ctx.sortDir() === 'asc' ? ' \u2191' : ' \u2193') : ''
211
- return el('th', { text: `${label}${icon}`, dataset: { sort: column }, style: { cursor: 'pointer' } })
212
- }
213
-
214
- function statusBadge(status) {
215
- return el('span', { className: `badge-status badge-${safeClassToken(status)}`, text: runtimeLabel('status', status) })
216
- }
217
-
218
- function gateProgressColor(progress) {
219
- if (progress === 100) return '#00dc82'
220
- return progress > 50 ? '#ffaa00' : '#ff4444'
221
- }
222
-
223
- function formatAction(action) {
224
- return runtimeLabel('action', action)
225
- }
226
-
227
- function nodeTooltip(params, artifacts) {
228
- if (params.dataType !== 'node') return ''
229
- const artifact = artifacts.find(candidate => candidate.id === params.data.id)
230
- return [artifact?.title ?? params.name, `${artifact?.type ?? '-'} - ${runtimeLabel('status', artifact?.status)}`, `v${artifact?.version ?? '?'}`].join('\n')
231
- }
232
-
233
- function truncate(value, maxLength) {
234
- const text = String(value ?? '')
235
- return text.length <= maxLength ? text : `${text.slice(0, Math.max(0, maxLength - 3))}...`
236
- }
237
-
238
- window.DashboardWorkflowRenderers = { renderCards, renderDependencyGraph, renderGateAnalysis, renderTable }
239
- })()
@@ -1,217 +0,0 @@
1
- /**
2
- * Workflow Page v3 - data flow and page events.
3
- */
4
- ;(() => {
5
- 'use strict'
6
-
7
- const { fetchJSON, t, $, $$, dom } = window.Dashboard
8
- const { el } = dom
9
- const renderers = window.DashboardWorkflowRenderers
10
-
11
- const STATUS_ORDER = ['BLOCKED', 'IN_PROGRESS', 'REVIEWING', 'PROPOSED', 'DRAFT', 'FROZEN', 'COMPLETED', 'DONE', 'APPROVED', 'REJECTED']
12
-
13
- let allArtifacts = []
14
- let currentState = null
15
- let filterStatus = 'all'
16
- let filterType = 'all'
17
- let filterText = ''
18
- let sortCol = null
19
- let sortDir = 'asc'
20
-
21
- async function renderWorkflow() {
22
- const app = $('#app')
23
- const statusFilter = selectFilter('wf-status-filter', `${t('common.all')} ${t('workflow.status')}`)
24
- const typeFilter = selectFilter('wf-type-filter', `${t('common.all')} ${t('workflow.type')}`)
25
- const searchInput = el('input', {
26
- id: 'wf-search',
27
- type: 'text',
28
- className: 'search-box',
29
- placeholder: `${t('common.search')}...`,
30
- value: filterText,
31
- style: { width: '200px' },
32
- })
33
-
34
- app.replaceChildren(
35
- el('div', { className: 'tabs', id: 'wf-tabs' }, [
36
- tabButton('cards', t('workflow.cards'), true),
37
- tabButton('table', t('workflow.table')),
38
- tabButton('graph', t('workflow.dependencyGraph')),
39
- tabButton('gates', t('workflow.gateAnalysis')),
40
- ]),
41
- el('div', {
42
- id: 'wf-filters',
43
- style: { display: 'flex', gap: '12px', marginBottom: '16px', alignItems: 'center', flexWrap: 'wrap' },
44
- }, [statusFilter, typeFilter, searchInput, el('span', { className: 'text-muted text-sm', id: 'wf-count' })]),
45
- el('div', { id: 'wf-content' }, [el('div', { className: 'loading-placeholder', text: t('common.loading') })])
46
- )
47
-
48
- currentState = await fetchJSON('/api/state')
49
- allArtifacts = flattenArtifacts(currentState?.artifacts ?? [])
50
- await hydrateActions(allArtifacts)
51
- populateFilters(statusFilter, typeFilter)
52
-
53
- let currentTab = 'cards'
54
- statusFilter.addEventListener('change', event => { filterStatus = event.target.value; renderCurrentTab() })
55
- typeFilter.addEventListener('change', event => { filterType = event.target.value; renderCurrentTab() })
56
- searchInput.addEventListener('input', event => { filterText = event.target.value.toLowerCase(); renderCurrentTab() })
57
- $('#wf-tabs').addEventListener('click', (event) => {
58
- const tab = event.target.dataset?.tab
59
- if (!tab) return
60
- currentTab = tab
61
- $$('#wf-tabs .tab').forEach(node => node.classList.toggle('active', node.dataset.tab === tab))
62
- renderCurrentTab()
63
- })
64
-
65
- function renderCurrentTab() {
66
- renderTab(currentTab, getFiltered(), currentState)
67
- }
68
-
69
- renderCurrentTab()
70
- }
71
-
72
- async function hydrateActions(artifacts) {
73
- await Promise.all(artifacts.map(async (artifact) => {
74
- try {
75
- const data = await fetchJSON(`/api/artifacts/${artifact.id}/actions`)
76
- artifact.availableActions = data?.actions ?? []
77
- } catch (error) {
78
- observeRecoverableError(error)
79
- artifact.availableActions = []
80
- }
81
- }))
82
- }
83
-
84
- function selectFilter(id, label) {
85
- return el('select', { id, className: 'search-box', style: { width: '140px' } }, [option('all', label)])
86
- }
87
-
88
- function tabButton(tab, label, active = false) {
89
- return el('div', { className: ['tab', active ? 'active' : ''].filter(Boolean).join(' '), text: label, dataset: { tab } })
90
- }
91
-
92
- function option(value, label) {
93
- return el('option', { value, text: label })
94
- }
95
-
96
- function populateFilters(statusSel, typeSel) {
97
- const statuses = new Set(allArtifacts.map(artifact => artifact.status).filter(Boolean))
98
- const types = new Set(allArtifacts.map(artifact => artifact.type).filter(Boolean))
99
- statusSel.append(...STATUS_ORDER.filter(status => statuses.has(status)).map(status => option(status, status)))
100
- statusSel.value = statuses.has(filterStatus) ? filterStatus : 'all'
101
- typeSel.append(...[...types].sort().map(type => option(type, type)))
102
- typeSel.value = types.has(filterType) ? filterType : 'all'
103
- }
104
-
105
- function getFiltered() {
106
- let result = allArtifacts
107
- if (filterStatus !== 'all') result = result.filter(artifact => artifact.status === filterStatus)
108
- if (filterType !== 'all') result = result.filter(artifact => artifact.type === filterType)
109
- if (filterText) {
110
- result = result.filter((artifact) => {
111
- const title = String(artifact.title ?? '').toLowerCase()
112
- const type = String(artifact.type ?? '').toLowerCase()
113
- return title.includes(filterText) || type.includes(filterText)
114
- })
115
- }
116
- if (!sortCol) return result
117
- return [...result].sort((left, right) => {
118
- const leftValue = left[sortCol] ?? ''
119
- const rightValue = right[sortCol] ?? ''
120
- const comparison = typeof leftValue === 'number' ? leftValue - rightValue : String(leftValue).localeCompare(String(rightValue))
121
- return sortDir === 'asc' ? comparison : -comparison
122
- })
123
- }
124
-
125
- function renderTab(tab, artifacts, state) {
126
- void state
127
- const container = $('#wf-content')
128
- const countNode = $('#wf-count')
129
- if (countNode) countNode.textContent = t('workflow.artifactCount', { count: artifacts.length })
130
- const ctx = {
131
- onSort: column => {
132
- if (sortCol === column) sortDir = sortDir === 'asc' ? 'desc' : 'asc'
133
- else { sortCol = column; sortDir = 'asc' }
134
- renderers.renderTable(container, getFiltered(), ctx)
135
- },
136
- sortCol: () => sortCol,
137
- sortDir: () => sortDir,
138
- wireActionButtons,
139
- }
140
- if (tab === 'table') return renderers.renderTable(container, artifacts, ctx)
141
- if (tab === 'graph') return renderers.renderDependencyGraph(container, artifacts)
142
- if (tab === 'gates') return renderers.renderGateAnalysis(container, artifacts)
143
- return renderers.renderCards(container, artifacts, ctx)
144
- }
145
-
146
- function showToast(msg, type = 'info') {
147
- let toast = $('#wf-toast')
148
- if (!toast) {
149
- toast = el('div', { id: 'wf-toast' })
150
- toast.style.cssText = 'position:fixed;top:70px;right:24px;padding:10px 18px;border-radius:8px;font-size:13px;z-index:999;transition:opacity 0.3s;opacity:0'
151
- document.body.appendChild(toast)
152
- }
153
- const colors = { info: '#5588ff', error: '#ff4444', success: '#00dc82' }
154
- toast.textContent = msg
155
- toast.style.background = colors[type] || colors.info
156
- toast.style.color = '#fff'
157
- toast.style.opacity = '1'
158
- setTimeout(() => { toast.style.opacity = '0' }, 3000)
159
- }
160
-
161
- function flattenArtifacts(roots) {
162
- const result = []
163
- const walk = (nodes, depth = 0) => {
164
- for (const node of nodes ?? []) {
165
- result.push({ ...node, depth })
166
- walk(node.children, depth + 1)
167
- }
168
- }
169
- walk(roots)
170
- return result
171
- }
172
-
173
- function wireActionButtons(container) {
174
- $$('.wf-action', container).forEach((button) => {
175
- button.addEventListener('click', async () => {
176
- const { id, action } = button.dataset
177
- const originalText = button.textContent
178
- button.disabled = true
179
- button.textContent = t('common.loading')
180
- try {
181
- const response = await fetch(`/api/artifacts/${id}/transition`, {
182
- method: 'POST',
183
- headers: { 'Content-Type': 'application/json' },
184
- body: JSON.stringify({ action }),
185
- })
186
- const data = await response.json()
187
- if (data.success) {
188
- showToast(`${t('workflow.gates')} \u2713`, 'success')
189
- renderWorkflow()
190
- } else {
191
- restoreActionButton(button, originalText)
192
- showToast(t('workflow.transitionFailed', { error: data.error }), 'error')
193
- }
194
- } catch (error) {
195
- restoreActionButton(button, originalText)
196
- showToast(t('workflow.error', { message: errorMessage(error) }), 'error')
197
- }
198
- })
199
- })
200
- }
201
-
202
- function restoreActionButton(button, text) {
203
- button.disabled = false
204
- button.textContent = text
205
- }
206
-
207
- function errorMessage(error) {
208
- return error instanceof Error ? error.message : String(error || 'Unknown error')
209
- }
210
-
211
- function observeRecoverableError(error) {
212
- void error
213
- }
214
-
215
- window.DashboardPages = window.DashboardPages || {}
216
- window.DashboardPages.workflow = renderWorkflow
217
- })()