@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.
Files changed (86) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-typecheck.log +4 -0
  3. package/LICENSE +21 -0
  4. package/dist/__tests__/dashboard.test.d.ts +2 -0
  5. package/dist/__tests__/dashboard.test.d.ts.map +1 -0
  6. package/dist/__tests__/dashboard.test.js +85 -0
  7. package/dist/__tests__/dashboard.test.js.map +1 -0
  8. package/dist/__tests__/exit-codes.test.d.ts +2 -0
  9. package/dist/__tests__/exit-codes.test.d.ts.map +1 -0
  10. package/dist/__tests__/exit-codes.test.js +73 -0
  11. package/dist/__tests__/exit-codes.test.js.map +1 -0
  12. package/dist/__tests__/store.test.d.ts +2 -0
  13. package/dist/__tests__/store.test.d.ts.map +1 -0
  14. package/dist/__tests__/store.test.js +169 -0
  15. package/dist/__tests__/store.test.js.map +1 -0
  16. package/dist/exit-codes.d.ts +14 -0
  17. package/dist/exit-codes.d.ts.map +1 -0
  18. package/dist/exit-codes.js +61 -0
  19. package/dist/exit-codes.js.map +1 -0
  20. package/dist/index.d.ts +21 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +20 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/persistence/dashboard/checks.d.ts +7 -0
  25. package/dist/persistence/dashboard/checks.d.ts.map +1 -0
  26. package/dist/persistence/dashboard/checks.js +279 -0
  27. package/dist/persistence/dashboard/checks.js.map +1 -0
  28. package/dist/persistence/dashboard/css.d.ts +6 -0
  29. package/dist/persistence/dashboard/css.d.ts.map +1 -0
  30. package/dist/persistence/dashboard/css.js +141 -0
  31. package/dist/persistence/dashboard/css.js.map +1 -0
  32. package/dist/persistence/dashboard/generator.d.ts +9 -0
  33. package/dist/persistence/dashboard/generator.d.ts.map +1 -0
  34. package/dist/persistence/dashboard/generator.js +79 -0
  35. package/dist/persistence/dashboard/generator.js.map +1 -0
  36. package/dist/persistence/dashboard/index.d.ts +5 -0
  37. package/dist/persistence/dashboard/index.d.ts.map +1 -0
  38. package/dist/persistence/dashboard/index.js +5 -0
  39. package/dist/persistence/dashboard/index.js.map +1 -0
  40. package/dist/persistence/dashboard/overview.d.ts +6 -0
  41. package/dist/persistence/dashboard/overview.d.ts.map +1 -0
  42. package/dist/persistence/dashboard/overview.js +65 -0
  43. package/dist/persistence/dashboard/overview.js.map +1 -0
  44. package/dist/persistence/dashboard/recipes.d.ts +6 -0
  45. package/dist/persistence/dashboard/recipes.d.ts.map +1 -0
  46. package/dist/persistence/dashboard/recipes.js +68 -0
  47. package/dist/persistence/dashboard/recipes.js.map +1 -0
  48. package/dist/persistence/dashboard/sessions.d.ts +6 -0
  49. package/dist/persistence/dashboard/sessions.d.ts.map +1 -0
  50. package/dist/persistence/dashboard/sessions.js +205 -0
  51. package/dist/persistence/dashboard/sessions.js.map +1 -0
  52. package/dist/persistence/dashboard/shared.d.ts +6 -0
  53. package/dist/persistence/dashboard/shared.d.ts.map +1 -0
  54. package/dist/persistence/dashboard/shared.js +211 -0
  55. package/dist/persistence/dashboard/shared.js.map +1 -0
  56. package/dist/persistence/dashboard/tool-tabs.d.ts +6 -0
  57. package/dist/persistence/dashboard/tool-tabs.d.ts.map +1 -0
  58. package/dist/persistence/dashboard/tool-tabs.js +102 -0
  59. package/dist/persistence/dashboard/tool-tabs.js.map +1 -0
  60. package/dist/persistence/store.d.ts +103 -0
  61. package/dist/persistence/store.d.ts.map +1 -0
  62. package/dist/persistence/store.js +156 -0
  63. package/dist/persistence/store.js.map +1 -0
  64. package/dist/types.d.ts +279 -0
  65. package/dist/types.d.ts.map +1 -0
  66. package/dist/types.js +2 -0
  67. package/dist/types.js.map +1 -0
  68. package/package.json +35 -0
  69. package/src/__tests__/dashboard.test.ts +102 -0
  70. package/src/__tests__/exit-codes.test.ts +87 -0
  71. package/src/__tests__/store.test.ts +213 -0
  72. package/src/exit-codes.ts +74 -0
  73. package/src/index.ts +71 -0
  74. package/src/persistence/dashboard/checks.ts +279 -0
  75. package/src/persistence/dashboard/css.ts +141 -0
  76. package/src/persistence/dashboard/generator.ts +89 -0
  77. package/src/persistence/dashboard/index.ts +5 -0
  78. package/src/persistence/dashboard/overview.ts +65 -0
  79. package/src/persistence/dashboard/recipes.ts +68 -0
  80. package/src/persistence/dashboard/sessions.ts +205 -0
  81. package/src/persistence/dashboard/shared.ts +211 -0
  82. package/src/persistence/dashboard/tool-tabs.ts +102 -0
  83. package/src/persistence/store.ts +233 -0
  84. package/src/types.ts +306 -0
  85. package/tsconfig.json +8 -0
  86. 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,6 @@
1
+ /**
2
+ * Session table + session detail rendering — used by fitness/sim tabs.
3
+ * Returns JS code as a string.
4
+ */
5
+ export declare function dashboardSessionsJs(): string;
6
+ //# sourceMappingURL=sessions.d.ts.map
@@ -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,6 @@
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 declare function dashboardSharedJs(): string;
6
+ //# sourceMappingURL=shared.d.ts.map
@@ -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,6 @@
1
+ /**
2
+ * Tool tab rendering — creates subtabs (Overview / Catalog / Recipes) under each tool tab.
3
+ * Returns JS code as a string.
4
+ */
5
+ export declare function dashboardToolTabsJs(): string;
6
+ //# sourceMappingURL=tool-tabs.d.ts.map
@@ -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"}