@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
|
@@ -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
|
-
})()
|