@opensip-tools/contracts 1.0.4
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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE +21 -0
- package/dist/__tests__/dashboard.test.d.ts +2 -0
- package/dist/__tests__/dashboard.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard.test.js +85 -0
- package/dist/__tests__/dashboard.test.js.map +1 -0
- package/dist/__tests__/exit-codes.test.d.ts +2 -0
- package/dist/__tests__/exit-codes.test.d.ts.map +1 -0
- package/dist/__tests__/exit-codes.test.js +73 -0
- package/dist/__tests__/exit-codes.test.js.map +1 -0
- package/dist/__tests__/store.test.d.ts +2 -0
- package/dist/__tests__/store.test.d.ts.map +1 -0
- package/dist/__tests__/store.test.js +169 -0
- package/dist/__tests__/store.test.js.map +1 -0
- package/dist/exit-codes.d.ts +14 -0
- package/dist/exit-codes.d.ts.map +1 -0
- package/dist/exit-codes.js +61 -0
- package/dist/exit-codes.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/persistence/dashboard/checks.d.ts +7 -0
- package/dist/persistence/dashboard/checks.d.ts.map +1 -0
- package/dist/persistence/dashboard/checks.js +279 -0
- package/dist/persistence/dashboard/checks.js.map +1 -0
- package/dist/persistence/dashboard/css.d.ts +6 -0
- package/dist/persistence/dashboard/css.d.ts.map +1 -0
- package/dist/persistence/dashboard/css.js +141 -0
- package/dist/persistence/dashboard/css.js.map +1 -0
- package/dist/persistence/dashboard/generator.d.ts +9 -0
- package/dist/persistence/dashboard/generator.d.ts.map +1 -0
- package/dist/persistence/dashboard/generator.js +79 -0
- package/dist/persistence/dashboard/generator.js.map +1 -0
- package/dist/persistence/dashboard/index.d.ts +5 -0
- package/dist/persistence/dashboard/index.d.ts.map +1 -0
- package/dist/persistence/dashboard/index.js +5 -0
- package/dist/persistence/dashboard/index.js.map +1 -0
- package/dist/persistence/dashboard/overview.d.ts +6 -0
- package/dist/persistence/dashboard/overview.d.ts.map +1 -0
- package/dist/persistence/dashboard/overview.js +65 -0
- package/dist/persistence/dashboard/overview.js.map +1 -0
- package/dist/persistence/dashboard/recipes.d.ts +6 -0
- package/dist/persistence/dashboard/recipes.d.ts.map +1 -0
- package/dist/persistence/dashboard/recipes.js +68 -0
- package/dist/persistence/dashboard/recipes.js.map +1 -0
- package/dist/persistence/dashboard/sessions.d.ts +6 -0
- package/dist/persistence/dashboard/sessions.d.ts.map +1 -0
- package/dist/persistence/dashboard/sessions.js +205 -0
- package/dist/persistence/dashboard/sessions.js.map +1 -0
- package/dist/persistence/dashboard/shared.d.ts +6 -0
- package/dist/persistence/dashboard/shared.d.ts.map +1 -0
- package/dist/persistence/dashboard/shared.js +211 -0
- package/dist/persistence/dashboard/shared.js.map +1 -0
- package/dist/persistence/dashboard/tool-tabs.d.ts +6 -0
- package/dist/persistence/dashboard/tool-tabs.d.ts.map +1 -0
- package/dist/persistence/dashboard/tool-tabs.js +102 -0
- package/dist/persistence/dashboard/tool-tabs.js.map +1 -0
- package/dist/persistence/store.d.ts +103 -0
- package/dist/persistence/store.d.ts.map +1 -0
- package/dist/persistence/store.js +156 -0
- package/dist/persistence/store.js.map +1 -0
- package/dist/types.d.ts +279 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +35 -0
- package/src/__tests__/dashboard.test.ts +102 -0
- package/src/__tests__/exit-codes.test.ts +87 -0
- package/src/__tests__/store.test.ts +213 -0
- package/src/exit-codes.ts +74 -0
- package/src/index.ts +71 -0
- package/src/persistence/dashboard/checks.ts +279 -0
- package/src/persistence/dashboard/css.ts +141 -0
- package/src/persistence/dashboard/generator.ts +89 -0
- package/src/persistence/dashboard/index.ts +5 -0
- package/src/persistence/dashboard/overview.ts +65 -0
- package/src/persistence/dashboard/recipes.ts +68 -0
- package/src/persistence/dashboard/sessions.ts +205 -0
- package/src/persistence/dashboard/shared.ts +211 -0
- package/src/persistence/dashboard/tool-tabs.ts +102 -0
- package/src/persistence/store.ts +233 -0
- package/src/types.ts +306 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipes.d.ts","sourceRoot":"","sources":["../../../src/persistence/dashboard/recipes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,wBAAgB,kBAAkB,IAAI,MAAM,CA8D3C"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipes catalog rendering — shows available recipes with their configuration.
|
|
3
|
+
* Returns JS code as a string.
|
|
4
|
+
*/
|
|
5
|
+
export function dashboardRecipesJs() {
|
|
6
|
+
return `
|
|
7
|
+
// =======================================================
|
|
8
|
+
// RECIPES CATALOG
|
|
9
|
+
// =======================================================
|
|
10
|
+
|
|
11
|
+
function renderRecipesPanel(container, recipesData) {
|
|
12
|
+
if (!recipesData || !recipesData.length) {
|
|
13
|
+
container.appendChild(el('div', {class:'empty', text:'No recipes available.'}));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const table = el('table', {class:'data-table'});
|
|
18
|
+
const thead = el('thead');
|
|
19
|
+
const headerRow = el('tr');
|
|
20
|
+
['Recipe', 'Description', 'Selector', 'Mode', 'Timeout', 'Tags'].forEach(h => {
|
|
21
|
+
headerRow.appendChild(el('th', {text: h}));
|
|
22
|
+
});
|
|
23
|
+
thead.appendChild(headerRow);
|
|
24
|
+
table.appendChild(thead);
|
|
25
|
+
|
|
26
|
+
const tbody = el('tbody');
|
|
27
|
+
recipesData.forEach(recipe => {
|
|
28
|
+
const row = el('tr');
|
|
29
|
+
|
|
30
|
+
// Name
|
|
31
|
+
const nameCell = el('td', {style:'font-weight:500'});
|
|
32
|
+
nameCell.appendChild(el('div', {text: recipe.displayName}));
|
|
33
|
+
nameCell.appendChild(el('div', {text: recipe.name, style:'font-size:11px;color:var(--text-dim);font-weight:400'}));
|
|
34
|
+
row.appendChild(nameCell);
|
|
35
|
+
|
|
36
|
+
// Description
|
|
37
|
+
row.appendChild(el('td', {text: recipe.description, style:'color:var(--text-muted)'}));
|
|
38
|
+
|
|
39
|
+
// Selector type
|
|
40
|
+
const selCell = el('td');
|
|
41
|
+
selCell.appendChild(el('span', {class:'badge', style:'background:var(--bg-hover);color:var(--text-muted)', text: recipe.selectorType}));
|
|
42
|
+
row.appendChild(selCell);
|
|
43
|
+
|
|
44
|
+
// Mode
|
|
45
|
+
const modeCell = el('td');
|
|
46
|
+
const modeColor = recipe.mode === 'parallel' ? 'color:var(--success)' : 'color:var(--warning)';
|
|
47
|
+
modeCell.appendChild(el('span', {text: recipe.mode, style: modeColor + ';font-size:12px'}));
|
|
48
|
+
row.appendChild(modeCell);
|
|
49
|
+
|
|
50
|
+
// Timeout
|
|
51
|
+
row.appendChild(el('td', {text: (recipe.timeout / 1000) + 's', style:'color:var(--text-dim);font-size:12px'}));
|
|
52
|
+
|
|
53
|
+
// Tags
|
|
54
|
+
const tagsCell = el('td');
|
|
55
|
+
(recipe.tags || []).forEach(t => {
|
|
56
|
+
tagsCell.appendChild(el('span', {class:'tag-badge', text: t}));
|
|
57
|
+
});
|
|
58
|
+
row.appendChild(tagsCell);
|
|
59
|
+
|
|
60
|
+
tbody.appendChild(row);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
table.appendChild(tbody);
|
|
64
|
+
container.appendChild(el('div', {class:'card'}, [table]));
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=recipes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipes.js","sourceRoot":"","sources":["../../../src/persistence/dashboard/recipes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,UAAU,kBAAkB;IAChC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4DR,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../../src/persistence/dashboard/sessions.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,wBAAgB,mBAAmB,IAAI,MAAM,CAuM5C"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session table + session detail rendering — used by fitness/sim tabs.
|
|
3
|
+
* Returns JS code as a string.
|
|
4
|
+
*/
|
|
5
|
+
export function dashboardSessionsJs() {
|
|
6
|
+
return String.raw `
|
|
7
|
+
// =======================================================
|
|
8
|
+
// SESSION TABLE (used by fitness/sim tabs)
|
|
9
|
+
// =======================================================
|
|
10
|
+
|
|
11
|
+
/** Derive 3-state session status: 'fail' | 'warn' | 'pass' */
|
|
12
|
+
function sessionStatus(s) {
|
|
13
|
+
if (s.summary.failed > 0) return 'fail';
|
|
14
|
+
if (s.summary.warnings > 0) return 'warn';
|
|
15
|
+
return 'pass';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function statusBadge(status) {
|
|
19
|
+
const labels = { fail: 'FAIL', warn: 'WARN', pass: 'PASS' };
|
|
20
|
+
const classes = { fail: 'badge-fail', warn: 'badge-warn', pass: 'badge-pass' };
|
|
21
|
+
return el('span', {class:'badge ' + classes[status], text: labels[status]});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function renderSessionTable(panel, toolSessions, accentColor) {
|
|
25
|
+
if (!toolSessions.length) {
|
|
26
|
+
panel.appendChild(el('div', {class:'empty', text:'No sessions yet.'}));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const tool = toolSessions[0].tool;
|
|
31
|
+
|
|
32
|
+
const table = el('table', {class:'data-table sortable'});
|
|
33
|
+
const thead = el('thead');
|
|
34
|
+
const headerRow = el('tr');
|
|
35
|
+
['Timestamp', 'Recipe', 'Pass Rate', 'Status', 'Passed', 'Failed', 'Findings', 'Duration'].forEach(h => {
|
|
36
|
+
headerRow.appendChild(el('th', {text: h}));
|
|
37
|
+
});
|
|
38
|
+
thead.appendChild(headerRow);
|
|
39
|
+
table.appendChild(thead);
|
|
40
|
+
|
|
41
|
+
const tbody = el('tbody');
|
|
42
|
+
toolSessions.forEach((s, idx) => {
|
|
43
|
+
const sc = s.score >= 90 ? 'color:var(--success)' : s.score >= 70 ? 'color:var(--warning)' : 'color:var(--error)';
|
|
44
|
+
const row = el('tr', {class:'clickable', id: 'session-row-' + tool + '-' + idx, onclick: () => {
|
|
45
|
+
tbody.querySelectorAll('tr.selected').forEach(r => r.classList.remove('selected'));
|
|
46
|
+
row.classList.add('selected');
|
|
47
|
+
renderDetail(s, idx);
|
|
48
|
+
}});
|
|
49
|
+
row.appendChild(el('td', {text: new Date(s.timestamp).toLocaleString()}));
|
|
50
|
+
row.appendChild(el('td', {text: s.recipe || 'default', style:'color:var(--text-muted)'}));
|
|
51
|
+
const scoreCell = el('td', {style: 'font-weight:600;' + sc});
|
|
52
|
+
scoreCell.textContent = s.score + '%';
|
|
53
|
+
row.appendChild(scoreCell);
|
|
54
|
+
const badgeCell = el('td');
|
|
55
|
+
badgeCell.appendChild(statusBadge(sessionStatus(s)));
|
|
56
|
+
row.appendChild(badgeCell);
|
|
57
|
+
row.appendChild(el('td', {text: ''+s.summary.passed, style:'color:var(--success)'}));
|
|
58
|
+
row.appendChild(el('td', {text: ''+s.summary.failed, style: s.summary.failed > 0 ? 'color:var(--error)' : 'color:var(--text-dim)'}));
|
|
59
|
+
row.appendChild(el('td', {text: ''+(s.summary.errors + (s.summary.warnings || 0))}));
|
|
60
|
+
row.appendChild(el('td', {text: (s.durationMs/1000).toFixed(1)+'s', style:'color:var(--text-dim)'}));
|
|
61
|
+
tbody.appendChild(row);
|
|
62
|
+
});
|
|
63
|
+
table.appendChild(tbody);
|
|
64
|
+
|
|
65
|
+
const sessionPag = el('div', {class:'pagination'});
|
|
66
|
+
const sec = el('div', {class:'section'}, [el('h3', {text:'Sessions (' + toolSessions.length + ')'}), el('div', {class:'card'}, [table, sessionPag])]);
|
|
67
|
+
panel.appendChild(sec);
|
|
68
|
+
paginateTable(tbody, sessionPag, 10);
|
|
69
|
+
|
|
70
|
+
// Detail container — kept as a direct reference, no global ID lookup needed
|
|
71
|
+
const detailContainer = el('div', {id: 'detail-' + tool + '-' + Math.random().toString(36).slice(2,8), class:'section', style:'display:none'});
|
|
72
|
+
panel.appendChild(detailContainer);
|
|
73
|
+
|
|
74
|
+
function renderDetail(session, idx) {
|
|
75
|
+
detailContainer.style.display = 'block';
|
|
76
|
+
while (detailContainer.firstChild) detailContainer.removeChild(detailContainer.firstChild);
|
|
77
|
+
|
|
78
|
+
// Compute session-level totals from check findings
|
|
79
|
+
let totalErrors = 0;
|
|
80
|
+
let totalWarnings = 0;
|
|
81
|
+
session.checks.forEach(c => {
|
|
82
|
+
if (c.findings) {
|
|
83
|
+
c.findings.forEach(f => {
|
|
84
|
+
if (f.severity === 'error') totalErrors++;
|
|
85
|
+
else if (f.severity === 'warning') totalWarnings++;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const headerRow = el('div', {style:'display:flex;align-items:center;justify-content:space-between;margin-bottom:16px'});
|
|
91
|
+
const headerLeft = el('div');
|
|
92
|
+
headerLeft.appendChild(el('h3', {text: 'Session Detail \u2014 ' + new Date(session.timestamp).toLocaleString(), style:'margin-bottom:4px'}));
|
|
93
|
+
const sub = el('div', {style:'color:var(--text-dim);font-size:12px'});
|
|
94
|
+
const countParts = [];
|
|
95
|
+
if (totalErrors > 0) countParts.push(totalErrors + ' error' + (totalErrors !== 1 ? 's' : ''));
|
|
96
|
+
if (totalWarnings > 0) countParts.push(totalWarnings + ' warning' + (totalWarnings !== 1 ? 's' : ''));
|
|
97
|
+
const countsStr = countParts.length > 0 ? ' \u2014 ' + countParts.join(', ') : '';
|
|
98
|
+
sub.textContent = session.cwd + (session.recipe ? ' \u2014 recipe: ' + session.recipe : '') + countsStr;
|
|
99
|
+
headerLeft.appendChild(sub);
|
|
100
|
+
headerRow.appendChild(headerLeft);
|
|
101
|
+
|
|
102
|
+
detailContainer.appendChild(headerRow);
|
|
103
|
+
const filterUid = 'df-' + tool + '-' + idx + '-' + Math.random().toString(36).slice(2,6);
|
|
104
|
+
|
|
105
|
+
// Check detail table
|
|
106
|
+
const table = el('table', {class:'data-table sortable'});
|
|
107
|
+
const thead = el('thead');
|
|
108
|
+
const thRow = el('tr');
|
|
109
|
+
['', 'Check', 'Status', 'Errors', 'Warnings', 'Findings', 'Duration'].forEach(h => {
|
|
110
|
+
thRow.appendChild(el('th', {text: h}));
|
|
111
|
+
});
|
|
112
|
+
thead.appendChild(thRow);
|
|
113
|
+
table.appendChild(thead);
|
|
114
|
+
|
|
115
|
+
const tbody = el('tbody');
|
|
116
|
+
const sortedChecks = [...session.checks].sort((a, b) => {
|
|
117
|
+
const aErrors = a.findings ? a.findings.filter(f => f.severity === 'error').length : 0;
|
|
118
|
+
const bErrors = b.findings ? b.findings.filter(f => f.severity === 'error').length : 0;
|
|
119
|
+
return bErrors - aErrors;
|
|
120
|
+
});
|
|
121
|
+
sortedChecks.forEach((check, i) => {
|
|
122
|
+
const checkErrors = check.findings ? check.findings.filter(f => f.severity === 'error').length : 0;
|
|
123
|
+
const checkWarnings = check.findings ? check.findings.filter(f => f.severity === 'warning').length : 0;
|
|
124
|
+
const findingsTotal = checkErrors + checkWarnings;
|
|
125
|
+
const hasFindings = findingsTotal > 0;
|
|
126
|
+
const expanderId = filterUid + '-exp-' + i;
|
|
127
|
+
const checkStatusVal = check.passed ? 'pass' : 'fail';
|
|
128
|
+
|
|
129
|
+
const arrowCell = el('td', {style:'width:24px;text-align:center;color:var(--text-dim);font-size:12px'});
|
|
130
|
+
if (hasFindings) arrowCell.textContent = '\u25B6';
|
|
131
|
+
|
|
132
|
+
const row = el('tr', {class: hasFindings ? 'clickable' : '', 'data-check-status': checkStatusVal, onclick: hasFindings ? () => {
|
|
133
|
+
const exp = document.getElementById(expanderId);
|
|
134
|
+
if (exp) {
|
|
135
|
+
const isOpen = exp.classList.toggle('open');
|
|
136
|
+
exp.style.display = isOpen ? 'table-row' : 'none';
|
|
137
|
+
arrowCell.textContent = isOpen ? '\u25BC' : '\u25B6';
|
|
138
|
+
}
|
|
139
|
+
row.classList.toggle('expanded');
|
|
140
|
+
} : undefined});
|
|
141
|
+
row.appendChild(arrowCell);
|
|
142
|
+
row.appendChild(el('td', {text: check.checkSlug, style:'font-weight:500'}));
|
|
143
|
+
|
|
144
|
+
const statusCell = el('td');
|
|
145
|
+
statusCell.appendChild(el('span', {class:'badge ' + (check.passed ? 'badge-pass' : 'badge-fail'), text: check.passed ? 'PASS' : 'FAIL'}));
|
|
146
|
+
row.appendChild(statusCell);
|
|
147
|
+
row.appendChild(el('td', {text: ''+checkErrors, style: checkErrors > 0 ? 'color:var(--error)' : 'color:var(--text-dim)'}));
|
|
148
|
+
row.appendChild(el('td', {text: ''+checkWarnings, style: checkWarnings > 0 ? 'color:var(--warning)' : 'color:var(--text-dim)'}));
|
|
149
|
+
row.appendChild(el('td', {text: ''+findingsTotal, style: findingsTotal > 0 ? 'color:var(--text)' : 'color:var(--text-dim)'}));
|
|
150
|
+
row.appendChild(el('td', {text: check.durationMs > 0 ? check.durationMs + 'ms' : '0ms', style:'color:var(--text-dim)'}));
|
|
151
|
+
tbody.appendChild(row);
|
|
152
|
+
|
|
153
|
+
if (hasFindings) {
|
|
154
|
+
const expRow = el('tr', {id: expanderId, class:'expander-row', 'data-check-status': checkStatusVal});
|
|
155
|
+
const expCell = el('td', {colspan:'7', style:'padding:0'});
|
|
156
|
+
const expContent = el('div', {class:'expander-content'});
|
|
157
|
+
|
|
158
|
+
const fTable = el('table', {class:'data-table', style:'margin:0;border:none'});
|
|
159
|
+
const fHead = el('thead');
|
|
160
|
+
const fHeaderRow = el('tr');
|
|
161
|
+
['Severity', 'Message', 'File', 'Suggestion'].forEach(h => {
|
|
162
|
+
fHeaderRow.appendChild(el('th', {text: h, style:'font-size:11px;padding:6px 12px'}));
|
|
163
|
+
});
|
|
164
|
+
fHead.appendChild(fHeaderRow);
|
|
165
|
+
fTable.appendChild(fHead);
|
|
166
|
+
|
|
167
|
+
const fBody = el('tbody');
|
|
168
|
+
check.findings.forEach(f => {
|
|
169
|
+
const fRow = el('tr');
|
|
170
|
+
const sevCell = el('td', {style:'padding:6px 12px'});
|
|
171
|
+
sevCell.appendChild(el('span', {class:'finding-sev ' + f.severity, text: f.severity}));
|
|
172
|
+
fRow.appendChild(sevCell);
|
|
173
|
+
fRow.appendChild(el('td', {text: f.message, style:'padding:6px 12px;font-size:13px'}));
|
|
174
|
+
fRow.appendChild(el('td', {text: f.filePath ? f.filePath + (f.line ? ':' + f.line : '') : '\u2014', style:'padding:6px 12px;color:var(--text-dim);font-size:12px'}));
|
|
175
|
+
fRow.appendChild(el('td', {text: f.suggestion || '\u2014', style:'padding:6px 12px;color:var(--accent);font-size:12px'}));
|
|
176
|
+
fBody.appendChild(fRow);
|
|
177
|
+
});
|
|
178
|
+
fTable.appendChild(fBody);
|
|
179
|
+
expContent.appendChild(fTable);
|
|
180
|
+
expCell.appendChild(expContent);
|
|
181
|
+
expRow.appendChild(expCell);
|
|
182
|
+
tbody.appendChild(expRow);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
table.appendChild(tbody);
|
|
186
|
+
const detailPag = el('div', {class:'pagination'});
|
|
187
|
+
detailContainer.appendChild(el('div', {class:'card'}, [table, detailPag]));
|
|
188
|
+
|
|
189
|
+
// Enable sorting on the detail table
|
|
190
|
+
makeSortable(table);
|
|
191
|
+
|
|
192
|
+
// Paginate
|
|
193
|
+
paginateGroupedRows(tbody, detailPag, 10);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Auto-show latest and highlight first row
|
|
197
|
+
renderDetail(toolSessions[0], 0);
|
|
198
|
+
const firstRow = tbody.querySelector('tr');
|
|
199
|
+
if (firstRow) firstRow.classList.add('selected');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../../src/persistence/dashboard/sessions.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,UAAU,mBAAmB;IACjC,OAAO,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqMlB,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../src/persistence/dashboard/shared.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,wBAAgB,iBAAiB,IAAI,MAAM,CA6M1C"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared dashboard JS — el() helper, pagination, tab switching.
|
|
3
|
+
* Returns JS code as a string to be inlined in the <script> block.
|
|
4
|
+
*/
|
|
5
|
+
export function dashboardSharedJs() {
|
|
6
|
+
return String.raw `
|
|
7
|
+
// Tab switching
|
|
8
|
+
document.getElementById('tab-bar').addEventListener('click', e => {
|
|
9
|
+
const tab = e.target.closest('.tab');
|
|
10
|
+
if (!tab) return;
|
|
11
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
12
|
+
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
|
13
|
+
tab.classList.add('active');
|
|
14
|
+
document.getElementById('panel-' + tab.dataset.tab).classList.add('active');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function el(tag, attrs, children) {
|
|
18
|
+
const e = document.createElement(tag);
|
|
19
|
+
if (attrs) Object.entries(attrs).forEach(([k,v]) => {
|
|
20
|
+
if (k === 'text') e.textContent = v;
|
|
21
|
+
else if (k === 'class') e.className = v;
|
|
22
|
+
else if (k.startsWith('on')) e.addEventListener(k.slice(2), v);
|
|
23
|
+
else e.setAttribute(k, v);
|
|
24
|
+
});
|
|
25
|
+
if (children) children.forEach(c => { if (typeof c === 'string') e.appendChild(document.createTextNode(c)); else if (c) e.appendChild(c); });
|
|
26
|
+
return e;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// =======================================================
|
|
30
|
+
// PAGINATION HELPERS
|
|
31
|
+
// =======================================================
|
|
32
|
+
|
|
33
|
+
function renderPageButtons(container, currentPage, totalPages, goToPage) {
|
|
34
|
+
container.appendChild(el('button', {class:'pagination-btn' + (currentPage === 0 ? ' disabled' : ''), text:'\u2190 Prev', onclick: () => { if (currentPage > 0) goToPage(currentPage - 1); }}));
|
|
35
|
+
|
|
36
|
+
const pages = [];
|
|
37
|
+
for (let p = 0; p < totalPages; p++) {
|
|
38
|
+
if (p < 2 || p >= totalPages - 2 || Math.abs(p - currentPage) <= 1) {
|
|
39
|
+
pages.push(p);
|
|
40
|
+
} else if (pages.length > 0 && pages[pages.length - 1] !== -1) {
|
|
41
|
+
pages.push(-1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pages.forEach(p => {
|
|
46
|
+
if (p === -1) {
|
|
47
|
+
container.appendChild(el('span', {style:'color:var(--text-dim);padding:4px 4px;font-size:12px', text:'\u2026'}));
|
|
48
|
+
} else {
|
|
49
|
+
container.appendChild(el('button', {class:'pagination-btn' + (p === currentPage ? ' active' : ''), text: ''+(p+1), onclick: () => goToPage(p)}));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
container.appendChild(el('button', {class:'pagination-btn' + (currentPage >= totalPages-1 ? ' disabled' : ''), text:'Next \u2192', onclick: () => { if (currentPage < totalPages-1) goToPage(currentPage + 1); }}));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function paginateTable(tbody, paginationContainer, pageSize) {
|
|
57
|
+
const rows = Array.from(tbody.children);
|
|
58
|
+
let currentPage = 0;
|
|
59
|
+
const totalPages = Math.max(1, Math.ceil(rows.length / pageSize));
|
|
60
|
+
|
|
61
|
+
function renderPage() {
|
|
62
|
+
const start = currentPage * pageSize;
|
|
63
|
+
const end = start + pageSize;
|
|
64
|
+
rows.forEach((row, i) => { row.style.display = (i >= start && i < end) ? '' : 'none'; });
|
|
65
|
+
|
|
66
|
+
while (paginationContainer.firstChild) paginationContainer.removeChild(paginationContainer.firstChild);
|
|
67
|
+
if (rows.length <= pageSize) return;
|
|
68
|
+
|
|
69
|
+
const info = el('div', {class:'pagination-info', text: 'Showing ' + (start+1) + '-' + Math.min(end, rows.length) + ' of ' + rows.length});
|
|
70
|
+
paginationContainer.appendChild(info);
|
|
71
|
+
|
|
72
|
+
const btns = el('div', {class:'pagination-btns'});
|
|
73
|
+
renderPageButtons(btns, currentPage, totalPages, (p) => { currentPage = p; renderPage(); });
|
|
74
|
+
paginationContainer.appendChild(btns);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
renderPage();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function paginateGroupedRows(tbody, paginationContainer, pageSize) {
|
|
81
|
+
const allRows = Array.from(tbody.children);
|
|
82
|
+
const groups = [];
|
|
83
|
+
for (let i = 0; i < allRows.length; i++) {
|
|
84
|
+
const row = allRows[i];
|
|
85
|
+
if (row.classList.contains('expander-row')) continue;
|
|
86
|
+
const group = [row];
|
|
87
|
+
if (i + 1 < allRows.length && allRows[i+1].classList.contains('expander-row')) {
|
|
88
|
+
group.push(allRows[i+1]);
|
|
89
|
+
}
|
|
90
|
+
groups.push(group);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let currentPage = 0;
|
|
94
|
+
const totalPages = Math.max(1, Math.ceil(groups.length / pageSize));
|
|
95
|
+
|
|
96
|
+
function renderPage() {
|
|
97
|
+
const start = currentPage * pageSize;
|
|
98
|
+
const end = start + pageSize;
|
|
99
|
+
groups.forEach((group, i) => {
|
|
100
|
+
const visible = i >= start && i < end;
|
|
101
|
+
group.forEach(row => {
|
|
102
|
+
if (row.classList.contains('expander-row')) {
|
|
103
|
+
row.dataset.paged = visible ? 'yes' : 'no';
|
|
104
|
+
if (!visible) row.style.display = 'none';
|
|
105
|
+
} else {
|
|
106
|
+
row.style.display = visible ? '' : 'none';
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
while (paginationContainer.firstChild) paginationContainer.removeChild(paginationContainer.firstChild);
|
|
112
|
+
if (groups.length <= pageSize) return;
|
|
113
|
+
|
|
114
|
+
const info = el('div', {class:'pagination-info', text: 'Showing ' + (start+1) + '-' + Math.min(end, groups.length) + ' of ' + groups.length + ' checks'});
|
|
115
|
+
paginationContainer.appendChild(info);
|
|
116
|
+
|
|
117
|
+
const btns = el('div', {class:'pagination-btns'});
|
|
118
|
+
renderPageButtons(btns, currentPage, totalPages, (p) => { currentPage = p; renderPage(); });
|
|
119
|
+
paginationContainer.appendChild(btns);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
renderPage();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// =======================================================
|
|
126
|
+
// SORTABLE TABLE COLUMNS
|
|
127
|
+
// =======================================================
|
|
128
|
+
|
|
129
|
+
function makeSortable(table) {
|
|
130
|
+
const thead = table.querySelector('thead');
|
|
131
|
+
const tbody = table.querySelector('tbody');
|
|
132
|
+
if (!thead || !tbody) return;
|
|
133
|
+
|
|
134
|
+
const headers = Array.from(thead.querySelectorAll('th'));
|
|
135
|
+
let sortCol = -1;
|
|
136
|
+
let sortAsc = true;
|
|
137
|
+
|
|
138
|
+
headers.forEach((th, colIdx) => {
|
|
139
|
+
if (!th.textContent.trim()) return; // skip empty headers (arrow column)
|
|
140
|
+
th.style.cursor = 'pointer';
|
|
141
|
+
th.style.userSelect = 'none';
|
|
142
|
+
th.addEventListener('click', () => {
|
|
143
|
+
if (sortCol === colIdx) {
|
|
144
|
+
sortAsc = !sortAsc;
|
|
145
|
+
} else {
|
|
146
|
+
sortCol = colIdx;
|
|
147
|
+
sortAsc = true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Update sort indicators
|
|
151
|
+
headers.forEach(h => { h.dataset.sort = ''; });
|
|
152
|
+
th.dataset.sort = sortAsc ? 'asc' : 'desc';
|
|
153
|
+
|
|
154
|
+
// Collect data rows with their expander rows
|
|
155
|
+
const allRows = Array.from(tbody.children);
|
|
156
|
+
const groups = [];
|
|
157
|
+
for (let i = 0; i < allRows.length; i++) {
|
|
158
|
+
const row = allRows[i];
|
|
159
|
+
if (row.classList.contains('expander-row')) continue;
|
|
160
|
+
const group = [row];
|
|
161
|
+
if (i + 1 < allRows.length && allRows[i+1].classList.contains('expander-row')) {
|
|
162
|
+
group.push(allRows[i+1]);
|
|
163
|
+
}
|
|
164
|
+
groups.push(group);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
groups.sort((a, b) => {
|
|
168
|
+
const aText = (a[0].children[colIdx]?.textContent || '').trim();
|
|
169
|
+
const bText = (b[0].children[colIdx]?.textContent || '').trim();
|
|
170
|
+
// Try numeric comparison
|
|
171
|
+
const aNum = parseFloat(aText);
|
|
172
|
+
const bNum = parseFloat(bText);
|
|
173
|
+
if (!isNaN(aNum) && !isNaN(bNum)) {
|
|
174
|
+
return sortAsc ? aNum - bNum : bNum - aNum;
|
|
175
|
+
}
|
|
176
|
+
// Date detection (contains / or -)
|
|
177
|
+
const aDate = Date.parse(aText);
|
|
178
|
+
const bDate = Date.parse(bText);
|
|
179
|
+
if (!isNaN(aDate) && !isNaN(bDate)) {
|
|
180
|
+
return sortAsc ? aDate - bDate : bDate - aDate;
|
|
181
|
+
}
|
|
182
|
+
// String comparison
|
|
183
|
+
return sortAsc ? aText.localeCompare(bText) : bText.localeCompare(aText);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Reorder DOM — append each group (data row + optional expander)
|
|
187
|
+
groups.forEach(group => {
|
|
188
|
+
group.forEach(row => tbody.appendChild(row));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Re-paginate if a pagination container exists after the table
|
|
192
|
+
const pagContainer = table.parentElement?.querySelector('.pagination');
|
|
193
|
+
if (pagContainer) {
|
|
194
|
+
const hasExpanders = groups.some(g => g.length > 1);
|
|
195
|
+
if (hasExpanders) {
|
|
196
|
+
paginateGroupedRows(tbody, pagContainer, 10);
|
|
197
|
+
} else {
|
|
198
|
+
paginateTable(tbody, pagContainer, 10);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// After all rendering: init sorting
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
document.querySelectorAll('.data-table.sortable').forEach(t => makeSortable(t));
|
|
208
|
+
}, 0);
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../../src/persistence/dashboard/shared.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2MlB,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-tabs.d.ts","sourceRoot":"","sources":["../../../src/persistence/dashboard/tool-tabs.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,wBAAgB,mBAAmB,IAAI,MAAM,CAgG5C"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool tab rendering — creates subtabs (Overview / Catalog / Recipes) under each tool tab.
|
|
3
|
+
* Returns JS code as a string.
|
|
4
|
+
*/
|
|
5
|
+
export function dashboardToolTabsJs() {
|
|
6
|
+
return `
|
|
7
|
+
// =======================================================
|
|
8
|
+
// TOOL SUBTAB RENDERING
|
|
9
|
+
// =======================================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Render a tool tab with subtabs: Overview | Catalog | Recipes
|
|
13
|
+
* @param panelId - e.g., 'panel-fitness'
|
|
14
|
+
* @param toolSessions - filtered sessions for this tool
|
|
15
|
+
* @param accentColor - CSS var for accent
|
|
16
|
+
* @param catalogLabel - e.g., 'Checks', 'Scenarios', 'Assessments'
|
|
17
|
+
* @param catalogData - check/scenario/assessment catalog entries (or empty)
|
|
18
|
+
* @param renderCatalogFn - function(container, data) to render the catalog
|
|
19
|
+
*/
|
|
20
|
+
function renderToolTab(panelId, toolSessions, accentColor, catalogLabel, catalogData, renderCatalogFn) {
|
|
21
|
+
const panel = document.getElementById(panelId);
|
|
22
|
+
|
|
23
|
+
// Subtab bar
|
|
24
|
+
const subtabBar = el('div', {class:'subtab-bar'});
|
|
25
|
+
const tabs = [
|
|
26
|
+
{ id: 'overview', label: 'Overview' },
|
|
27
|
+
{ id: 'catalog', label: catalogLabel },
|
|
28
|
+
{ id: 'recipes', label: 'Recipes' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const panels = {};
|
|
32
|
+
tabs.forEach((t, i) => {
|
|
33
|
+
const subtab = el('div', {
|
|
34
|
+
class: 'subtab' + (i === 0 ? ' active' : ''),
|
|
35
|
+
'data-subtab': t.id,
|
|
36
|
+
text: t.label,
|
|
37
|
+
});
|
|
38
|
+
subtabBar.appendChild(subtab);
|
|
39
|
+
|
|
40
|
+
const subpanel = el('div', {
|
|
41
|
+
class: 'subtab-panel' + (i === 0 ? ' active' : ''),
|
|
42
|
+
id: panelId + '-' + t.id,
|
|
43
|
+
});
|
|
44
|
+
panels[t.id] = subpanel;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
panel.appendChild(subtabBar);
|
|
48
|
+
tabs.forEach(t => panel.appendChild(panels[t.id]));
|
|
49
|
+
|
|
50
|
+
// Subtab switching
|
|
51
|
+
subtabBar.addEventListener('click', e => {
|
|
52
|
+
const tab = e.target.closest('.subtab');
|
|
53
|
+
if (!tab) return;
|
|
54
|
+
subtabBar.querySelectorAll('.subtab').forEach(t => t.classList.remove('active'));
|
|
55
|
+
tab.classList.add('active');
|
|
56
|
+
tabs.forEach(t => panels[t.id].classList.remove('active'));
|
|
57
|
+
panels[tab.dataset.subtab].classList.add('active');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Render Overview subtab (sessions + detail)
|
|
61
|
+
renderSessionTable(panels['overview'], toolSessions, accentColor);
|
|
62
|
+
|
|
63
|
+
// Render Catalog subtab
|
|
64
|
+
if (catalogData && catalogData.length > 0) {
|
|
65
|
+
renderCatalogFn(panels['catalog'], catalogData);
|
|
66
|
+
} else {
|
|
67
|
+
panels['catalog'].appendChild(el('div', {class:'empty', text:'No ' + catalogLabel.toLowerCase() + ' available yet.'}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Render Recipes subtab
|
|
71
|
+
renderRecipesPanel(panels['recipes'], recipeCatalog);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// =======================================================
|
|
75
|
+
// RENDER ALL TOOL TABS
|
|
76
|
+
// =======================================================
|
|
77
|
+
|
|
78
|
+
function renderFitnessTab() {
|
|
79
|
+
renderToolTab(
|
|
80
|
+
'panel-fitness',
|
|
81
|
+
fitSessions,
|
|
82
|
+
'var(--accent-fitness)',
|
|
83
|
+
'Checks',
|
|
84
|
+
checkCatalog,
|
|
85
|
+
function(container, data) { renderChecksCatalog(container, data); }
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderSimulationTab() {
|
|
90
|
+
renderToolTab(
|
|
91
|
+
'panel-simulation',
|
|
92
|
+
simSessions,
|
|
93
|
+
'var(--accent-sim)',
|
|
94
|
+
'Scenarios',
|
|
95
|
+
[], // No scenarios yet
|
|
96
|
+
function(container, data) {}
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=tool-tabs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-tabs.js","sourceRoot":"","sources":["../../../src/persistence/dashboard/tool-tabs.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,UAAU,mBAAmB;IACjC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8FR,CAAC;AACF,CAAC"}
|